├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── stream.rs └── watch.rs ├── inotify-rs.sublime-project ├── src ├── events.rs ├── fd_guard.rs ├── inotify.rs ├── lib.rs ├── stream.rs ├── util.rs └── watches.rs └── tests └── main.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | env: 4 | MSRV: "1.70" 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | check: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: dtolnay/rust-toolchain@beta 14 | with: 15 | components: clippy, rustfmt 16 | - uses: Swatinem/rust-cache@v2 17 | - name: cargo fmt 18 | run: cargo fmt --all --check 19 | - name: clippy 20 | run: cargo clippy --all-targets --all-features -- -D warnings 21 | 22 | check-docs: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: dtolnay/rust-toolchain@stable 27 | - uses: Swatinem/rust-cache@v2 28 | - name: cargo doc 29 | env: 30 | RUSTDOCFLAGS: "-D rustdoc::all -A rustdoc::private-doc-tests" 31 | run: cargo doc --all-features --no-deps 32 | 33 | build: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: dtolnay/rust-toolchain@stable 38 | - uses: Swatinem/rust-cache@v2 39 | - name: Check rustc and cargo version 40 | run: rustc -V && cargo -V 41 | - name: Build 42 | run: cargo build --all-features --all-targets --verbose 43 | - name: Run tests 44 | run: cargo test --all-features --all-targets --verbose 45 | - name: Build (no default features) 46 | run: cargo build --no-default-features --all-targets --verbose 47 | - name: Run tests (no default features) 48 | run: cargo test --no-default-features --all-targets --verbose 49 | 50 | miri: 51 | name: "Miri" 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v4 55 | - name: Install Miri 56 | run: | 57 | rustup toolchain install nightly --component miri 58 | rustup override set nightly 59 | cargo miri setup 60 | - name: Test with Miri tests with "from_buffer" in name 61 | run: cargo miri test from_buffer 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | /**/target 3 | *.sublime-workspace 4 | *.swp 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.11.0 (2024-08-19) 4 | 5 | - Fix link in README ([#209]) 6 | - **Breaking change:** Make `bits` field of `EventMask`/`WatchMask` inaccessible. You can use the `.bits()` method instead. ([#211], [#218]) 7 | - Fix various links in documentation ([#213]) 8 | - Bump minimum supported Rust version (MSRV) to 1.70. ([#219]) 9 | 10 | [#209]: https://github.com/hannobraun/inotify-rs/pull/209 11 | [#211]: https://github.com/hannobraun/inotify-rs/pull/211 12 | [#213]: https://github.com/hannobraun/inotify-rs/pull/213 13 | [#218]: https://github.com/hannobraun/inotify-rs/pull/218 14 | [#219]: https://github.com/hannobraun/inotify-rs/pull/219 15 | 16 | 17 | ## v0.10.2 (2023-07-27) 18 | 19 | - Fix broken links to `Watches` in documentation ([#205]) 20 | 21 | [#205]: https://github.com/hannobraun/inotify-rs/pull/205 22 | 23 | 24 | ## v0.10.1 (2023-06-07) 25 | 26 | - Add `WatchDescriptor::get_watch_descriptor_id` ([#193]) 27 | - Add `Event::to_owned` ([#196]) 28 | - Deprecate `Event::into_owned` ([#196]) 29 | - Add `Watches`/`Inotify::watches`/`EventStream::watches` ([#197]) 30 | - Deprecate `Inotify::add_watch`/`Inotify::rm_watch` ([#197]) 31 | - Add `Inotify::into_event_stream`/`EventStream::into_inotify` ([#199]) 32 | - Deprecate `Inotify::event_stream` ([#199]) 33 | - Implement `AsFd` and bidirectional conversion to/from `OwnedFd` ([#202]) 34 | - Raise Minimum Supported Rust Version (MSRV) to 1.63.0 ([#202]) 35 | 36 | [#193]: https://github.com/hannobraun/inotify-rs/pull/193 37 | [#196]: https://github.com/hannobraun/inotify-rs/pull/196 38 | [#197]: https://github.com/hannobraun/inotify-rs/pull/197 39 | [#199]: https://github.com/hannobraun/inotify-rs/pull/199 40 | [#202]: https://github.com/hannobraun/inotify-rs/pull/202 41 | 42 | 43 | ## v0.10.0 (2021-12-07) 44 | 45 | - **Breaking change:** Remove special handling of `WouldBlock` error ([#190]) 46 | 47 | [#190]: https://github.com/hannobraun/inotify-rs/pull/190 48 | 49 | 50 | ## v0.9.6 (2021-11-03) 51 | 52 | - Fix build status badge in README ([#185]) 53 | - Add `get_buffer_size`/`get_absolute_path_buffer_size` ([#187]) 54 | 55 | [#185]: https://github.com/hannobraun/inotify-rs/pull/185 56 | [#187]: https://github.com/hannobraun/inotify-rs/pull/187 57 | 58 | 59 | ## v0.9.5 (2021-10-07) 60 | 61 | - Implement `Ord`/`PartialOrd` for `WatchDescriptor` ([#183]) 62 | 63 | [#183]: https://github.com/hannobraun/inotify-rs/pull/183 64 | 65 | 66 | ## v0.9.4 (2021-09-22) 67 | 68 | - Make `Event::into_owned` always available ([#179]) 69 | - Implement missing `Debug` implementations ([#180]) 70 | 71 | [#179]: https://github.com/hannobraun/inotify-rs/pull/179 72 | [#180]: https://github.com/hannobraun/inotify-rs/pull/180 73 | 74 | 75 | ## v0.9.3 (2021-05-12) 76 | 77 | - Improve documentation ([#167], [#169]) 78 | - Add missing check for invalid file descriptor ([#168]) 79 | - Fix unsound use of buffers due to misalignment ([#171]) 80 | - Add missing error checks ([#173]) 81 | 82 | [#167]: https://github.com/hannobraun/inotify-rs/pull/167 83 | [#168]: https://github.com/hannobraun/inotify-rs/pull/168 84 | [#169]: https://github.com/hannobraun/inotify-rs/pull/169 85 | [#171]: https://github.com/hannobraun/inotify-rs/pull/171 86 | [#173]: https://github.com/hannobraun/inotify-rs/pull/173 87 | 88 | 89 | ## v0.9.2 (2020-12-30) 90 | 91 | - Upgrade to Tokio 1.0 ([#165]) 92 | 93 | [#165]: https://github.com/hannobraun/inotify/pull/165 94 | 95 | 96 | ## v0.9.1 (2020-11-09) 97 | 98 | - Fix take wake-up ([#161]) 99 | 100 | [#161]: https://github.com/hannobraun/inotify/pull/161 101 | 102 | 103 | ## v0.9.0 (2020-11-06) 104 | 105 | - Update minimum supported Rust version to version 1.47 ([#154]) 106 | - Fix documentation: `Inotify::read_events` doesn't handle all events ([#157]) 107 | - Update to tokio 0.3 ([#158]) 108 | 109 | [#154]: https://github.com/hannobraun/inotify/pull/154 110 | [#157]: https://github.com/hannobraun/inotify/pull/157 111 | [#158]: https://github.com/hannobraun/inotify/pull/158 112 | 113 | 114 | ## v0.8.3 (2020-06-05) 115 | 116 | - Avoid using `inotify_init1` ([#146]) 117 | 118 | [#146]: https://github.com/hannobraun/inotify/pull/146 119 | 120 | 121 | ## v0.8.2 (2020-01-25) 122 | 123 | - Ensure file descriptor is closed on drop ([#140]) 124 | 125 | [#140]: https://github.com/inotify-rs/inotify/pull/140 126 | 127 | 128 | ## v0.8.1 (2020-01-23) 129 | 130 | No changes, due to a mistake made while releasing this version. 131 | 132 | 133 | ## v0.8.0 (2019-12-04) 134 | 135 | - Update to tokio 0.2 and futures 0.3 ([#134]) 136 | 137 | [#134]: https://github.com/inotify-rs/inotify/pull/134 138 | 139 | 140 | ## v0.7.1 (2020-06-05) 141 | 142 | - backport: Avoid using `inotify_init1` ([#146]) 143 | 144 | [#146]: https://github.com/hannobraun/inotify/pull/146 145 | 146 | 147 | ## v0.7.0 (2019-02-09) 148 | 149 | - Make stream API more flexible in regards to buffers ([ea3e7a394bf34a6ccce4f2136c0991fe7e8f1f42](ea3e7a394bf34a6ccce4f2136c0991fe7e8f1f42)) (breaking change) 150 | 151 | 152 | ## v0.6.1 (2018-08-28) 153 | 154 | - Don't return spurious filenames ([2f37560f](2f37560f)) 155 | 156 | 157 | ## v0.6.0 (2018-08-16) 158 | 159 | - Handle closing of inotify instance better ([824160fe](824160fe)) 160 | - Implement `EventStream` using `mio` ([ba4cb8c7](ba4cb8c7)) 161 | 162 | 163 | ## v0.5.1 (2018-02-27) 164 | 165 | - Add future-based async API ([569e65a7](569e65a7), closes [#49](49)) 166 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to inotify-rs 2 | 3 | Thank you for considering to work on inotify-rs. We're always happy to see outside contributions, small or large. 4 | 5 | You probably found this document in the repository of either the [inotify] or [inotify-sys] crate. Both are part of the same project, so this guide is valid for both (in fact, the documents in either repository should be identical). 6 | 7 | ## Opening issues 8 | 9 | If you found a problem with inotify-rs, please open an issue to let us know. If you're not sure whether you found a problem or not, just open an issue anyway. We'd rather close a few invalid issues than miss real problems. 10 | 11 | Issues are tracked on GitHub, in the repository for the respective crate: 12 | - [Open an inotify issue](https://github.com/inotify-rs/inotify/issues/new) 13 | - [Open an inotify-sys issue](https://github.com/inotify-rs/inotify-sys/issues/new) 14 | 15 | If you're unsure where to open your issue, just open it in the [inotify] repository. 16 | 17 | ## Contributing changes 18 | 19 | If you want to make a change to the inotify-rs code, please open a pull request on the respective repository. The best way to open a pull request is usually to just push a branch to your fork, and click the button that should appear near the top of your fork's GitHub page. 20 | 21 | If you're having any problems with completing your change, feel free to open a pull request anyway and ask any questions there. We're happy to help with getting changes across the finish line. 22 | 23 | 24 | [inotify]: https://github.com/hannobraun/inotify 25 | [inotify-sys]: https://github.com/hannobraun/inotify-sys 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "inotify" 4 | version = "0.11.0" 5 | authors = [ 6 | "Hanno Braun ", 7 | "Félix Saparelli ", 8 | "Cristian Kubis ", 9 | "Frank Denis " 10 | ] 11 | edition = "2018" 12 | rust-version = "1.70" 13 | 14 | description = "Idiomatic wrapper for inotify" 15 | documentation = "https://docs.rs/inotify" 16 | repository = "https://github.com/hannobraun/inotify" 17 | license = "ISC" 18 | readme = "README.md" 19 | 20 | keywords = ["inotify", "linux"] 21 | categories = ["api-bindings", "filesystem"] 22 | exclude = ["/.travis.yml", "/inotify-rs.sublime-project"] 23 | 24 | [badges] 25 | maintenance = { status = "actively-developed" } 26 | travis-ci = { repository = "inotify-rs/inotify" } 27 | 28 | 29 | [features] 30 | default = ["stream"] 31 | stream = ["futures-core", "tokio"] 32 | 33 | 34 | [dependencies] 35 | bitflags = "2" 36 | futures-core = { version = "0.3.30", optional = true } 37 | inotify-sys = "0.1.5" 38 | libc = "0.2" 39 | tokio = { version = "1.40.0", optional = true, features = ["net"] } 40 | 41 | [dev-dependencies] 42 | maplit = "1.0" 43 | rand = "0.8" 44 | tempfile = "3.12.0" 45 | futures-util = "0.3.30" 46 | tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } 47 | 48 | [[example]] 49 | name = "stream" 50 | required-features = ["stream"] 51 | 52 | [[example]] 53 | name = "watch" 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Hanno Braun and contributors 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 11 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 12 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 13 | THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # inotify-rs [![crates.io](https://img.shields.io/crates/v/inotify.svg)](https://crates.io/crates/inotify) [![Documentation](https://docs.rs/inotify/badge.svg)](https://docs.rs/inotify) [![Rust](https://github.com/hannobraun/inotify-rs/actions/workflows/rust.yml/badge.svg)](https://github.com/hannobraun/inotify-rs/actions/workflows/rust.yml) 2 | 3 | ## Introduce 4 | Idiomatic [inotify] wrapper for the [Rust programming language].This package generally tries to adhere to the underlying inotify API closely, while making access to it safe and convenient. 5 | 6 | ## Examples 7 | Now inotify-rs supports synchronous or asynchronous event monitoring. 8 | An example of synchronous is as follows: 9 | ```rs 10 | use inotify::{EventMask, Inotify, WatchMask}; 11 | use std::env; 12 | 13 | fn main() { 14 | let mut inotify = Inotify::init().expect("Failed to initialize inotify"); 15 | 16 | let current_dir = env::current_dir().expect("Failed to determine current directory"); 17 | 18 | inotify 19 | .watches() 20 | .add( 21 | current_dir, 22 | WatchMask::MODIFY | WatchMask::CREATE | WatchMask::DELETE, 23 | ) 24 | .expect("Failed to add inotify watch"); 25 | 26 | println!("Watching current directory for activity..."); 27 | 28 | let mut buffer = [0u8; 4096]; 29 | loop { 30 | let events = inotify 31 | .read_events_blocking(&mut buffer) 32 | .expect("Failed to read inotify events"); 33 | 34 | for event in events { 35 | if event.mask.contains(EventMask::CREATE) { 36 | if event.mask.contains(EventMask::ISDIR) { 37 | println!("Directory created: {:?}", event.name); 38 | } else { 39 | println!("File created: {:?}", event.name); 40 | } 41 | } else if event.mask.contains(EventMask::DELETE) { 42 | if event.mask.contains(EventMask::ISDIR) { 43 | println!("Directory deleted: {:?}", event.name); 44 | } else { 45 | println!("File deleted: {:?}", event.name); 46 | } 47 | } else if event.mask.contains(EventMask::MODIFY) { 48 | if event.mask.contains(EventMask::ISDIR) { 49 | println!("Directory modified: {:?}", event.name); 50 | } else { 51 | println!("File modified: {:?}", event.name); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | ``` 58 | Perhaps you want asynchronous monitoring of events.An example of asynchronous is as follows: 59 | ```rs 60 | use std::{fs::File, io, thread, time::Duration}; 61 | 62 | use futures_util::StreamExt; 63 | use inotify::{Inotify, WatchMask}; 64 | use tempfile::TempDir; 65 | 66 | #[tokio::main] 67 | async fn main() -> Result<(), io::Error> { 68 | let inotify = Inotify::init().expect("Failed to initialize inotify"); 69 | 70 | let dir = TempDir::new()?; 71 | // Watch for modify and create events. 72 | inotify 73 | .watches() 74 | .add(dir.path(), WatchMask::CREATE | WatchMask::MODIFY)?; 75 | // Create a thread to operate on the target directory 76 | thread::spawn::<_, Result<(), io::Error>>(move || loop { 77 | File::create(dir.path().join("file"))?; 78 | thread::sleep(Duration::from_millis(500)); 79 | }); 80 | 81 | let mut buffer = [0; 1024]; 82 | let mut stream = inotify.into_event_stream(&mut buffer)?; 83 | // Read events from async stream 84 | while let Some(event_or_error) = stream.next().await { 85 | println!("event: {:?}", event_or_error?); 86 | } 87 | 88 | Ok(()) 89 | } 90 | 91 | 92 | ``` 93 | 94 | ## Usage 95 | 96 | Add the following to your `Cargo.toml`: 97 | 98 | ```toml 99 | [dependencies] 100 | inotify = "0.11" 101 | ``` 102 | 103 | Please refer to the [documentation] and the example above, for information on how to use it in your code. 104 | 105 | ## Notice 106 | Please note that inotify-rs is a relatively low-level wrapper around the original inotify API. And, of course, it is Linux-specific, just like inotify itself. If you are looking for a higher-level and platform-independent file system notification library, please consider **[notify]**. 107 | 108 | If you need to access inotify in a way that this wrapper doesn't support, consider using **[inotify-sys]** instead. 109 | 110 | ## Documentation 111 | 112 | The most important piece of documentation for inotify-rs is the **[API reference]**, as it contains a thorough description of the complete API, as well as examples. 113 | 114 | Additional examples can be found in the **[examples directory]**. 115 | 116 | Please also make sure to read the **[inotify man page]**. Inotify use can be hard to get right, and this low-level wrapper won't protect you from all mistakes. 117 | 118 | ## License 119 | 120 | Copyright (c) Hanno Braun and contributors 121 | 122 | Permission to use, copy, modify, and/or distribute this software for any purpose 123 | with or without fee is hereby granted, provided that the above copyright notice 124 | and this permission notice appear in all copies. 125 | 126 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 127 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 128 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 129 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 130 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 131 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 132 | THIS SOFTWARE. 133 | 134 | [inotify]: http://en.wikipedia.org/wiki/Inotify 135 | [Rust programming language]: http://rust-lang.org/ 136 | [documentation]: https://docs.rs/inotify 137 | [notify]: https://crates.io/crates/notify 138 | [inotify-sys]: https://crates.io/crates/inotify-sys 139 | [API reference]: https://docs.rs/inotify 140 | [examples directory]: https://github.com/inotify-rs/inotify/tree/main/examples 141 | [inotify man page]: http://man7.org/linux/man-pages/man7/inotify.7.html 142 | -------------------------------------------------------------------------------- /examples/stream.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io, thread, time::Duration}; 2 | 3 | use futures_util::StreamExt; 4 | use inotify::{Inotify, WatchMask}; 5 | use tempfile::TempDir; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<(), io::Error> { 9 | let inotify = Inotify::init().expect("Failed to initialize inotify"); 10 | 11 | let dir = TempDir::new()?; 12 | 13 | inotify 14 | .watches() 15 | .add(dir.path(), WatchMask::CREATE | WatchMask::MODIFY)?; 16 | 17 | thread::spawn::<_, Result<(), io::Error>>(move || loop { 18 | File::create(dir.path().join("file"))?; 19 | thread::sleep(Duration::from_millis(500)); 20 | }); 21 | 22 | let mut buffer = [0; 1024]; 23 | let mut stream = inotify.into_event_stream(&mut buffer)?; 24 | 25 | while let Some(event_or_error) = stream.next().await { 26 | println!("event: {:?}", event_or_error?); 27 | } 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /examples/watch.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use inotify::{EventMask, Inotify, WatchMask}; 4 | 5 | fn main() { 6 | let mut inotify = Inotify::init().expect("Failed to initialize inotify"); 7 | 8 | let current_dir = env::current_dir().expect("Failed to determine current directory"); 9 | 10 | inotify 11 | .watches() 12 | .add( 13 | current_dir, 14 | WatchMask::MODIFY | WatchMask::CREATE | WatchMask::DELETE, 15 | ) 16 | .expect("Failed to add inotify watch"); 17 | 18 | println!("Watching current directory for activity..."); 19 | 20 | let mut buffer = [0u8; 4096]; 21 | loop { 22 | let events = inotify 23 | .read_events_blocking(&mut buffer) 24 | .expect("Failed to read inotify events"); 25 | 26 | for event in events { 27 | if event.mask.contains(EventMask::CREATE) { 28 | if event.mask.contains(EventMask::ISDIR) { 29 | println!("Directory created: {:?}", event.name); 30 | } else { 31 | println!("File created: {:?}", event.name); 32 | } 33 | } else if event.mask.contains(EventMask::DELETE) { 34 | if event.mask.contains(EventMask::ISDIR) { 35 | println!("Directory deleted: {:?}", event.name); 36 | } else { 37 | println!("File deleted: {:?}", event.name); 38 | } 39 | } else if event.mask.contains(EventMask::MODIFY) { 40 | if event.mask.contains(EventMask::ISDIR) { 41 | println!("Directory modified: {:?}", event.name); 42 | } else { 43 | println!("File modified: {:?}", event.name); 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /inotify-rs.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": ".", 6 | "file_exclude_patterns": [ "target/*" ] 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/events.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | convert::{TryFrom, TryInto}, 3 | error::Error, 4 | ffi::{OsStr, OsString}, 5 | fmt::Display, 6 | mem, 7 | os::unix::ffi::OsStrExt, 8 | sync::Weak, 9 | }; 10 | 11 | use inotify_sys as ffi; 12 | 13 | use crate::fd_guard::FdGuard; 14 | use crate::watches::WatchDescriptor; 15 | 16 | /// Iterator over inotify events 17 | /// 18 | /// Allows for iteration over the events returned by 19 | /// [`Inotify::read_events_blocking`] or [`Inotify::read_events`]. 20 | /// 21 | /// [`Inotify::read_events_blocking`]: crate::Inotify::read_events_blocking 22 | /// [`Inotify::read_events`]: crate::Inotify::read_events 23 | #[derive(Debug)] 24 | pub struct Events<'a> { 25 | fd: Weak, 26 | buffer: &'a [u8], 27 | num_bytes: usize, 28 | pos: usize, 29 | } 30 | 31 | impl<'a> Events<'a> { 32 | pub(crate) fn new(fd: Weak, buffer: &'a [u8], num_bytes: usize) -> Self { 33 | Events { 34 | fd, 35 | buffer, 36 | num_bytes, 37 | pos: 0, 38 | } 39 | } 40 | } 41 | 42 | impl<'a> Iterator for Events<'a> { 43 | type Item = Event<&'a OsStr>; 44 | 45 | fn next(&mut self) -> Option { 46 | if self.pos < self.num_bytes { 47 | let (step, event) = Event::from_buffer(self.fd.clone(), &self.buffer[self.pos..]); 48 | self.pos += step; 49 | 50 | Some(event) 51 | } else { 52 | None 53 | } 54 | } 55 | } 56 | 57 | /// An inotify event 58 | /// 59 | /// A file system event that describes a change that the user previously 60 | /// registered interest in. To watch for events, call [`Watches::add`]. To 61 | /// retrieve events, call [`Inotify::read_events_blocking`] or 62 | /// [`Inotify::read_events`]. 63 | /// 64 | /// [`Watches::add`]: crate::Watches::add 65 | /// [`Inotify::read_events_blocking`]: crate::Inotify::read_events_blocking 66 | /// [`Inotify::read_events`]: crate::Inotify::read_events 67 | #[derive(Clone, Debug)] 68 | pub struct Event { 69 | /// Identifies the watch this event originates from 70 | /// 71 | /// This [`WatchDescriptor`] is equal to the one that [`Watches::add`] 72 | /// returned when interest for this event was registered. The 73 | /// [`WatchDescriptor`] can be used to remove the watch using 74 | /// [`Watches::remove`], thereby preventing future events of this type 75 | /// from being created. 76 | /// 77 | /// [`Watches::add`]: crate::Watches::add 78 | /// [`Watches::remove`]: crate::Watches::remove 79 | pub wd: WatchDescriptor, 80 | 81 | /// Indicates what kind of event this is 82 | pub mask: EventMask, 83 | 84 | /// Connects related events to each other 85 | /// 86 | /// When a file is renamed, this results two events: [`MOVED_FROM`] and 87 | /// [`MOVED_TO`]. The `cookie` field will be the same for both of them, 88 | /// thereby making is possible to connect the event pair. 89 | /// 90 | /// [`MOVED_FROM`]: EventMask::MOVED_FROM 91 | /// [`MOVED_TO`]: EventMask::MOVED_TO 92 | pub cookie: u32, 93 | 94 | /// The name of the file the event originates from 95 | /// 96 | /// This field is set only if the subject of the event is a file or directory in a 97 | /// watched directory. If the event concerns a file or directory that is 98 | /// watched directly, `name` will be `None`. 99 | pub name: Option, 100 | } 101 | 102 | impl<'a> Event<&'a OsStr> { 103 | fn new(fd: Weak, event: &ffi::inotify_event, name: &'a OsStr) -> Self { 104 | let mask = EventMask::from_bits(event.mask) 105 | .expect("Failed to convert event mask. This indicates a bug."); 106 | 107 | let wd = crate::WatchDescriptor { id: event.wd, fd }; 108 | 109 | let name = if name.is_empty() { None } else { Some(name) }; 110 | 111 | Event { 112 | wd, 113 | mask, 114 | cookie: event.cookie, 115 | name, 116 | } 117 | } 118 | 119 | /// Create an `Event` from a buffer 120 | /// 121 | /// Assumes that a full `inotify_event` plus its name is located at the 122 | /// beginning of `buffer`. 123 | /// 124 | /// Returns the number of bytes used from the buffer, and the event. 125 | /// 126 | /// # Panics 127 | /// 128 | /// Panics if the buffer does not contain a full event, including its name. 129 | pub(crate) fn from_buffer(fd: Weak, buffer: &'a [u8]) -> (usize, Self) { 130 | let event_size = mem::size_of::(); 131 | 132 | // Make sure that the buffer is big enough to contain an event, without 133 | // the name. Otherwise we can't safely convert it to an `inotify_event`. 134 | assert!(buffer.len() >= event_size); 135 | 136 | let ffi_event_ptr = buffer.as_ptr() as *const ffi::inotify_event; 137 | 138 | // We have a pointer to an `inotify_event`, pointing to the beginning of 139 | // `buffer`. Since we know, as per the assertion above, that there are 140 | // enough bytes in the buffer for at least one event, we can safely 141 | // read that `inotify_event`. 142 | // We call `read_unaligned()` since the byte buffer has alignment 1 143 | // and `inotify_event` has a higher alignment, so `*` cannot be used to dereference 144 | // the unaligned pointer (undefined behavior). 145 | let ffi_event = unsafe { ffi_event_ptr.read_unaligned() }; 146 | 147 | // The name's length is given by `event.len`. There should always be 148 | // enough bytes left in the buffer to fit the name. Let's make sure that 149 | // is the case. 150 | let bytes_left_in_buffer = buffer.len() - event_size; 151 | assert!(bytes_left_in_buffer >= ffi_event.len as usize); 152 | 153 | // Directly after the event struct should be a name, if there's one 154 | // associated with the event. Let's make a new slice that starts with 155 | // that name. If there's no name, this slice might have a length of `0`. 156 | let bytes_consumed = event_size + ffi_event.len as usize; 157 | let name = &buffer[event_size..bytes_consumed]; 158 | 159 | // Remove trailing '\0' bytes 160 | // 161 | // The events in the buffer are aligned, and `name` is filled up 162 | // with '\0' up to the alignment boundary. Here we remove those 163 | // additional bytes. 164 | // 165 | // The `unwrap` here is safe, because `splitn` always returns at 166 | // least one result, even if the original slice contains no '\0'. 167 | let name = name.splitn(2, |b| b == &0u8).next().unwrap(); 168 | 169 | let event = Event::new(fd, &ffi_event, OsStr::from_bytes(name)); 170 | 171 | (bytes_consumed, event) 172 | } 173 | 174 | /// Returns an owned copy of the event. 175 | #[deprecated = "use `to_owned()` instead; methods named `into_owned()` usually take self by value"] 176 | #[allow(clippy::wrong_self_convention)] 177 | pub fn into_owned(&self) -> EventOwned { 178 | self.to_owned() 179 | } 180 | 181 | /// Returns an owned copy of the event. 182 | #[must_use = "cloning is often expensive and is not expected to have side effects"] 183 | pub fn to_owned(&self) -> EventOwned { 184 | Event { 185 | wd: self.wd.clone(), 186 | mask: self.mask, 187 | cookie: self.cookie, 188 | name: self.name.map(OsStr::to_os_string), 189 | } 190 | } 191 | } 192 | 193 | /// An owned version of `Event` 194 | pub type EventOwned = Event; 195 | 196 | bitflags! { 197 | /// Indicates the type of an event 198 | /// 199 | /// This struct can be retrieved from an [`Event`] via its `mask` field. 200 | /// You can determine the [`Event`]'s type by comparing the `EventMask` to 201 | /// its associated constants. 202 | /// 203 | /// Please refer to the documentation of [`Event`] for a usage example. 204 | #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)] 205 | pub struct EventMask: u32 { 206 | /// File was accessed 207 | /// 208 | /// When watching a directory, this event is only triggered for objects 209 | /// inside the directory, not the directory itself. 210 | /// 211 | /// See [`inotify_sys::IN_ACCESS`]. 212 | const ACCESS = ffi::IN_ACCESS; 213 | 214 | /// Metadata (permissions, timestamps, ...) changed 215 | /// 216 | /// When watching a directory, this event can be triggered for the 217 | /// directory itself, as well as objects inside the directory. 218 | /// 219 | /// See [`inotify_sys::IN_ATTRIB`]. 220 | const ATTRIB = ffi::IN_ATTRIB; 221 | 222 | /// File opened for writing was closed 223 | /// 224 | /// When watching a directory, this event is only triggered for objects 225 | /// inside the directory, not the directory itself. 226 | /// 227 | /// See [`inotify_sys::IN_CLOSE_WRITE`]. 228 | const CLOSE_WRITE = ffi::IN_CLOSE_WRITE; 229 | 230 | /// File or directory not opened for writing was closed 231 | /// 232 | /// When watching a directory, this event can be triggered for the 233 | /// directory itself, as well as objects inside the directory. 234 | /// 235 | /// See [`inotify_sys::IN_CLOSE_NOWRITE`]. 236 | const CLOSE_NOWRITE = ffi::IN_CLOSE_NOWRITE; 237 | 238 | /// File/directory created in watched directory 239 | /// 240 | /// When watching a directory, this event is only triggered for objects 241 | /// inside the directory, not the directory itself. 242 | /// 243 | /// See [`inotify_sys::IN_CREATE`]. 244 | const CREATE = ffi::IN_CREATE; 245 | 246 | /// File/directory deleted from watched directory 247 | /// 248 | /// When watching a directory, this event is only triggered for objects 249 | /// inside the directory, not the directory itself. 250 | const DELETE = ffi::IN_DELETE; 251 | 252 | /// Watched file/directory was deleted 253 | /// 254 | /// See [`inotify_sys::IN_DELETE_SELF`]. 255 | const DELETE_SELF = ffi::IN_DELETE_SELF; 256 | 257 | /// File was modified 258 | /// 259 | /// When watching a directory, this event is only triggered for objects 260 | /// inside the directory, not the directory itself. 261 | /// 262 | /// See [`inotify_sys::IN_MODIFY`]. 263 | const MODIFY = ffi::IN_MODIFY; 264 | 265 | /// Watched file/directory was moved 266 | /// 267 | /// See [`inotify_sys::IN_MOVE_SELF`]. 268 | const MOVE_SELF = ffi::IN_MOVE_SELF; 269 | 270 | /// File was renamed/moved; watched directory contained old name 271 | /// 272 | /// When watching a directory, this event is only triggered for objects 273 | /// inside the directory, not the directory itself. 274 | /// 275 | /// See [`inotify_sys::IN_MOVED_FROM`]. 276 | const MOVED_FROM = ffi::IN_MOVED_FROM; 277 | 278 | /// File was renamed/moved; watched directory contains new name 279 | /// 280 | /// When watching a directory, this event is only triggered for objects 281 | /// inside the directory, not the directory itself. 282 | /// 283 | /// See [`inotify_sys::IN_MOVED_TO`]. 284 | const MOVED_TO = ffi::IN_MOVED_TO; 285 | 286 | /// File or directory was opened 287 | /// 288 | /// When watching a directory, this event can be triggered for the 289 | /// directory itself, as well as objects inside the directory. 290 | /// 291 | /// See [`inotify_sys::IN_OPEN`]. 292 | const OPEN = ffi::IN_OPEN; 293 | 294 | /// Watch was removed 295 | /// 296 | /// This event will be generated, if the watch was removed explicitly 297 | /// (via [`Watches::remove`]), or automatically (because the file was 298 | /// deleted or the file system was unmounted). 299 | /// 300 | /// See [`inotify_sys::IN_IGNORED`]. 301 | /// 302 | /// [`Watches::remove`]: crate::Watches::remove 303 | const IGNORED = ffi::IN_IGNORED; 304 | 305 | /// Event related to a directory 306 | /// 307 | /// The subject of the event is a directory. 308 | /// 309 | /// See [`inotify_sys::IN_ISDIR`]. 310 | const ISDIR = ffi::IN_ISDIR; 311 | 312 | /// Event queue overflowed 313 | /// 314 | /// The event queue has overflowed and events have presumably been lost. 315 | /// 316 | /// See [`inotify_sys::IN_Q_OVERFLOW`]. 317 | const Q_OVERFLOW = ffi::IN_Q_OVERFLOW; 318 | 319 | /// File system containing watched object was unmounted. 320 | /// File system was unmounted 321 | /// 322 | /// The file system that contained the watched object has been 323 | /// unmounted. An event with [`EventMask::IGNORED`] will subsequently be 324 | /// generated for the same watch descriptor. 325 | /// 326 | /// See [`inotify_sys::IN_UNMOUNT`]. 327 | const UNMOUNT = ffi::IN_UNMOUNT; 328 | } 329 | } 330 | 331 | impl EventMask { 332 | /// Parse this event mask into a ParsedEventMask 333 | pub fn parse(self) -> Result { 334 | self.try_into() 335 | } 336 | 337 | /// Wrapper around [`Self::from_bits_retain`] for backwards compatibility 338 | /// 339 | /// # Safety 340 | /// 341 | /// This function is not actually unsafe. It is just a wrapper around the 342 | /// safe [`Self::from_bits_retain`]. 343 | #[deprecated = "Use the safe `from_bits_retain` method instead"] 344 | pub unsafe fn from_bits_unchecked(bits: u32) -> Self { 345 | Self::from_bits_retain(bits) 346 | } 347 | } 348 | 349 | /// A struct that provides structured access to event masks 350 | /// returned from reading an event from an inotify fd 351 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 352 | pub struct ParsedEventMask { 353 | /// The kind of event that occurred 354 | /// 355 | /// Inotify events that come from the kernel have 356 | /// exactly 0 or 1 of the flags associated with the 357 | /// event type set. 358 | pub kind: Option, 359 | /// The auxiliary flags about the event 360 | pub auxiliary_flags: EventAuxiliaryFlags, 361 | } 362 | 363 | impl ParsedEventMask { 364 | /// Construct a `ParsedEventMask` from its component parts 365 | pub fn from_parts(kind: Option, auxiliary_flags: EventAuxiliaryFlags) -> Self { 366 | ParsedEventMask { 367 | kind, 368 | auxiliary_flags, 369 | } 370 | } 371 | 372 | /// Parse a raw event mask 373 | pub fn from_raw_event_mask(mask: EventMask) -> Result { 374 | if mask.contains(EventMask::Q_OVERFLOW) { 375 | return Err(EventMaskParseError::QueueOverflow); 376 | } 377 | 378 | let kind = Option::::try_from(mask)?; 379 | let auxiliary_flags = EventAuxiliaryFlags::from(mask); 380 | 381 | Ok(ParsedEventMask::from_parts(kind, auxiliary_flags)) 382 | } 383 | } 384 | 385 | impl TryFrom for ParsedEventMask { 386 | type Error = EventMaskParseError; 387 | 388 | fn try_from(value: EventMask) -> Result { 389 | Self::from_raw_event_mask(value) 390 | } 391 | } 392 | 393 | /// Represents the type of inotify event 394 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 395 | pub enum EventKind { 396 | /// File was accessed (e.g., [`read(2)`], [`execve(2)`]) 397 | /// 398 | /// [`read(2)`]: https://man7.org/linux/man-pages/man2/read.2.html 399 | /// [`execve(2)`]: https://man7.org/linux/man-pages/man2/execve.2.html 400 | Access, 401 | 402 | /// Metadata changed—for example, permissions (e.g., 403 | /// [`chmod(2)`]), timestamps (e.g., [`utimensat(2)`]), extended 404 | /// attributes ([`setxattr(2)`]), link count (since Linux 405 | /// 2.6.25; e.g., for the target of [`link(2)`] and for 406 | /// [`unlink(2)`]), and user/group ID (e.g., [`chown(2)`]) 407 | /// 408 | /// [`chmod(2)`]: https://man7.org/linux/man-pages/man2/chmod.2.html 409 | /// [`utimensat(2)`]: https://man7.org/linux/man-pages/man2/utimensat.2.html 410 | /// [`setxattr(2)`]: https://man7.org/linux/man-pages/man2/setxattr.2.html 411 | /// [`link(2)`]: https://man7.org/linux/man-pages/man2/link.2.html 412 | /// [`unlink(2)`]: https://man7.org/linux/man-pages/man2/unlink.2.html 413 | /// [`chown(2)`]: https://man7.org/linux/man-pages/man2/chown.2.html 414 | Attrib, 415 | 416 | /// File opened for writing was closed 417 | CloseWrite, 418 | 419 | /// File or directory not opened for writing was closed 420 | CloseNowrite, 421 | 422 | /// File/directory created in watched directory (e.g., 423 | /// [`open(2)`] **O_CREAT**, [`mkdir(2)`], [`link(2)`], [`symlink(2)`], [`bind(2)`] 424 | /// on a UNIX domain socket) 425 | /// 426 | /// [`open(2)`]: https://man7.org/linux/man-pages/man2/open.2.html 427 | /// [`mkdir(2)`]: https://man7.org/linux/man-pages/man2/mkdir.2.html 428 | /// [`link(2)`]: https://man7.org/linux/man-pages/man2/link.2.html 429 | /// [`symlink(2)`]: https://man7.org/linux/man-pages/man2/symlink.2.html 430 | /// [`bind(2)`]: https://man7.org/linux/man-pages/man2/bind.2.html 431 | Create, 432 | 433 | /// File/directory deleted from watched directory 434 | Delete, 435 | 436 | /// Watched file/directory was itself deleted. (This event 437 | /// also occurs if an object is moved to another 438 | /// filesystem, since [`mv(1)`] in effect copies the file to 439 | /// the other filesystem and then deletes it from the 440 | /// original filesystem.) 441 | /// 442 | /// [`mv(1)`]: https://man7.org/linux/man-pages/man1/mv.1.html 443 | DeleteSelf, 444 | 445 | /// File was modified (e.g., [`write(2)`], [`truncate(2)`]) 446 | /// 447 | /// [`write(2)`]: https://man7.org/linux/man-pages/man2/write.2.html 448 | /// [`truncate(2)`]: https://man7.org/linux/man-pages/man2/truncate.2.html 449 | Modify, 450 | 451 | /// Watched file/directory was itself moved 452 | MoveSelf, 453 | 454 | /// Generated for the directory containing the old filename when a file is renamed 455 | MovedFrom, 456 | 457 | /// Generated for the directory containing the new filename when a file is renamed 458 | MovedTo, 459 | 460 | /// File or directory was opened 461 | Open, 462 | } 463 | 464 | impl EventKind { 465 | const BITFLAG_ENUM_MAP: &[(EventMask, EventKind)] = &[ 466 | (EventMask::ACCESS, EventKind::Access), 467 | (EventMask::ATTRIB, EventKind::Attrib), 468 | (EventMask::CLOSE_WRITE, EventKind::CloseWrite), 469 | (EventMask::CLOSE_NOWRITE, EventKind::CloseNowrite), 470 | (EventMask::CREATE, EventKind::Create), 471 | (EventMask::DELETE, EventKind::Delete), 472 | (EventMask::DELETE_SELF, EventKind::DeleteSelf), 473 | (EventMask::MODIFY, EventKind::Modify), 474 | (EventMask::MOVE_SELF, EventKind::MoveSelf), 475 | (EventMask::MOVED_FROM, EventKind::MovedFrom), 476 | (EventMask::MOVED_TO, EventKind::MovedTo), 477 | (EventMask::OPEN, EventKind::Open), 478 | ]; 479 | 480 | /// Parse the auxiliary flags from a raw event mask 481 | pub fn from_raw_event_mask(mask: EventMask) -> Result, EventMaskParseError> { 482 | let mut kinds = Self::BITFLAG_ENUM_MAP.iter().filter_map(|bf_map| { 483 | if mask.contains(bf_map.0) { 484 | Some(bf_map.1) 485 | } else { 486 | None 487 | } 488 | }); 489 | 490 | // Optionally take the first matching bitflag 491 | let kind = kinds.next(); 492 | 493 | if kinds.next().is_some() { 494 | // The mask is invalid. 495 | // 496 | // More than one of the bitflags are set 497 | return Err(EventMaskParseError::TooManyBitsSet(mask)); 498 | } 499 | 500 | Ok(kind) 501 | } 502 | } 503 | 504 | impl TryFrom for Option { 505 | type Error = EventMaskParseError; 506 | 507 | fn try_from(value: EventMask) -> Result { 508 | EventKind::from_raw_event_mask(value) 509 | } 510 | } 511 | 512 | /// Auxiliary flags for inotify events 513 | /// 514 | /// The non-mutually-exclusive bitflags that may be set 515 | /// in an event read from an inotify fd. 0 or more of these 516 | /// bitflags may be set. 517 | #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)] 518 | pub struct EventAuxiliaryFlags { 519 | /// Watch was removed when explicitly removed via [`inotify_rm_watch(2)`] 520 | /// or automatically (because the file was deleted or the filesystem was unmounted) 521 | /// 522 | /// [`inotify_rm_watch(2)`]: https://man7.org/linux/man-pages/man2/inotify_rm_watch.2.html 523 | pub ignored: bool, 524 | 525 | /// Event subject is a directory rather than a regular file 526 | pub isdir: bool, 527 | 528 | /// File system containing watched object was unmounted 529 | /// 530 | /// An event with **IN_IGNORED** will subsequently be generated for the same watch descriptor. 531 | pub unmount: bool, 532 | } 533 | 534 | impl EventAuxiliaryFlags { 535 | /// Parse the auxiliary flags from a raw event mask 536 | pub fn from_raw_event_mask(mask: EventMask) -> Self { 537 | EventAuxiliaryFlags { 538 | ignored: mask.contains(EventMask::IGNORED), 539 | isdir: mask.contains(EventMask::ISDIR), 540 | unmount: mask.contains(EventMask::UNMOUNT), 541 | } 542 | } 543 | } 544 | 545 | impl From for EventAuxiliaryFlags { 546 | fn from(value: EventMask) -> Self { 547 | Self::from_raw_event_mask(value) 548 | } 549 | } 550 | 551 | /// An error that occured from parsing an raw event mask 552 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 553 | pub enum EventMaskParseError { 554 | /// More than one bit repesenting the event type was set 555 | TooManyBitsSet(EventMask), 556 | /// The event is a signal that the kernels event queue overflowed 557 | QueueOverflow, 558 | } 559 | 560 | impl Display for EventMaskParseError { 561 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 562 | match self { 563 | Self::TooManyBitsSet(mask) => { 564 | writeln!( 565 | f, 566 | "Error parsing event mask: too many event type bits set | {mask:?}" 567 | ) 568 | } 569 | Self::QueueOverflow => writeln!(f, "Error: the kernel's event queue overflowed"), 570 | } 571 | } 572 | } 573 | 574 | impl Error for EventMaskParseError {} 575 | 576 | #[cfg(test)] 577 | mod tests { 578 | use std::{io::prelude::*, mem, slice, sync}; 579 | 580 | use inotify_sys as ffi; 581 | 582 | use crate::{EventMask, EventMaskParseError}; 583 | 584 | use super::{Event, EventAuxiliaryFlags, EventKind, ParsedEventMask}; 585 | 586 | #[test] 587 | fn from_buffer_should_not_mistake_next_event_for_name_of_previous_event() { 588 | let mut buffer = [0u8; 1024]; 589 | 590 | // First, put a normal event into the buffer 591 | let event = ffi::inotify_event { 592 | wd: 0, 593 | mask: 0, 594 | cookie: 0, 595 | len: 0, // no name following after event 596 | }; 597 | let event = unsafe { 598 | slice::from_raw_parts(&event as *const _ as *const u8, mem::size_of_val(&event)) 599 | }; 600 | (&mut buffer[..]) 601 | .write_all(event) 602 | .expect("Failed to write into buffer"); 603 | 604 | // After that event, simulate an event that starts with a non-zero byte. 605 | buffer[mem::size_of_val(event)] = 1; 606 | 607 | // Now create the event and verify that the name is actually `None`, as 608 | // dictated by the value `len` above. 609 | let (_, event) = Event::from_buffer(sync::Weak::new(), &buffer); 610 | assert_eq!(event.name, None); 611 | } 612 | 613 | #[test] 614 | fn parse_event_kinds() { 615 | // Parse each event kind 616 | for bf_map in EventKind::BITFLAG_ENUM_MAP { 617 | assert_eq!( 618 | Ok(ParsedEventMask { 619 | kind: Some(bf_map.1), 620 | auxiliary_flags: Default::default() 621 | }), 622 | bf_map.0.parse() 623 | ); 624 | } 625 | 626 | // Parse an event with no event kind 627 | assert_eq!( 628 | Ok(ParsedEventMask { 629 | kind: None, 630 | auxiliary_flags: Default::default() 631 | }), 632 | EventMask::from_bits_retain(0).parse() 633 | ) 634 | } 635 | 636 | #[test] 637 | fn parse_event_auxiliary_flags() { 638 | assert_eq!( 639 | Ok(ParsedEventMask { 640 | kind: None, 641 | auxiliary_flags: EventAuxiliaryFlags { 642 | ignored: true, 643 | isdir: false, 644 | unmount: false 645 | } 646 | }), 647 | EventMask::IGNORED.parse() 648 | ); 649 | 650 | assert_eq!( 651 | Ok(ParsedEventMask { 652 | kind: None, 653 | auxiliary_flags: EventAuxiliaryFlags { 654 | ignored: false, 655 | isdir: true, 656 | unmount: false 657 | } 658 | }), 659 | EventMask::ISDIR.parse() 660 | ); 661 | 662 | assert_eq!( 663 | Ok(ParsedEventMask { 664 | kind: None, 665 | auxiliary_flags: EventAuxiliaryFlags { 666 | ignored: false, 667 | isdir: false, 668 | unmount: true 669 | } 670 | }), 671 | EventMask::UNMOUNT.parse() 672 | ); 673 | } 674 | 675 | #[test] 676 | fn parse_event_errors() { 677 | assert_eq!( 678 | Err(EventMaskParseError::QueueOverflow), 679 | EventMask::Q_OVERFLOW.parse() 680 | ); 681 | 682 | let mask = EventMask::ATTRIB | EventMask::ACCESS; 683 | assert_eq!(Err(EventMaskParseError::TooManyBitsSet(mask)), mask.parse()); 684 | } 685 | } 686 | -------------------------------------------------------------------------------- /src/fd_guard.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ops::Deref, 3 | os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, RawFd}, 4 | sync::atomic::{AtomicBool, Ordering}, 5 | }; 6 | 7 | use inotify_sys as ffi; 8 | 9 | /// A RAII guard around a `RawFd` that closes it automatically on drop. 10 | #[derive(Debug)] 11 | pub struct FdGuard { 12 | pub(crate) fd: RawFd, 13 | pub(crate) close_on_drop: AtomicBool, 14 | } 15 | 16 | impl FdGuard { 17 | /// Indicate that the wrapped file descriptor should _not_ be closed 18 | /// when the guard is dropped. 19 | /// 20 | /// This should be called in cases where ownership of the wrapped file 21 | /// descriptor has been "moved" out of the guard. 22 | /// 23 | /// This is factored out into a separate function to ensure that it's 24 | /// always used consistently. 25 | #[inline] 26 | pub fn should_not_close(&self) { 27 | self.close_on_drop.store(false, Ordering::Release); 28 | } 29 | } 30 | 31 | impl Deref for FdGuard { 32 | type Target = RawFd; 33 | 34 | #[inline] 35 | fn deref(&self) -> &Self::Target { 36 | &self.fd 37 | } 38 | } 39 | 40 | impl Drop for FdGuard { 41 | fn drop(&mut self) { 42 | if self.close_on_drop.load(Ordering::Acquire) { 43 | unsafe { 44 | ffi::close(self.fd); 45 | } 46 | } 47 | } 48 | } 49 | 50 | impl FromRawFd for FdGuard { 51 | unsafe fn from_raw_fd(fd: RawFd) -> Self { 52 | FdGuard { 53 | fd, 54 | close_on_drop: AtomicBool::new(true), 55 | } 56 | } 57 | } 58 | 59 | impl IntoRawFd for FdGuard { 60 | fn into_raw_fd(self) -> RawFd { 61 | self.should_not_close(); 62 | self.fd 63 | } 64 | } 65 | 66 | impl AsRawFd for FdGuard { 67 | fn as_raw_fd(&self) -> RawFd { 68 | self.fd 69 | } 70 | } 71 | 72 | impl AsFd for FdGuard { 73 | #[inline] 74 | fn as_fd(&self) -> BorrowedFd<'_> { 75 | unsafe { BorrowedFd::borrow_raw(self.fd) } 76 | } 77 | } 78 | 79 | impl PartialEq for FdGuard { 80 | fn eq(&self, other: &FdGuard) -> bool { 81 | self.fd == other.fd 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/inotify.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd}, 4 | path::Path, 5 | sync::{atomic::AtomicBool, Arc}, 6 | }; 7 | 8 | use inotify_sys as ffi; 9 | use libc::{fcntl, F_GETFL, F_SETFL, O_NONBLOCK}; 10 | 11 | use crate::events::Events; 12 | use crate::fd_guard::FdGuard; 13 | use crate::util::read_into_buffer; 14 | use crate::watches::{WatchDescriptor, WatchMask, Watches}; 15 | 16 | #[cfg(feature = "stream")] 17 | use crate::stream::EventStream; 18 | 19 | /// Idiomatic Rust wrapper around Linux's inotify API 20 | /// 21 | /// `Inotify` is a wrapper around an inotify instance. It generally tries to 22 | /// adhere to the underlying inotify API closely, while making access to it 23 | /// safe and convenient. 24 | /// 25 | /// Please refer to the [top-level documentation] for further details and a 26 | /// usage example. 27 | /// 28 | /// [top-level documentation]: crate 29 | #[derive(Debug)] 30 | pub struct Inotify { 31 | fd: Arc, 32 | } 33 | 34 | impl Inotify { 35 | /// Creates an [`Inotify`] instance 36 | /// 37 | /// Initializes an inotify instance by calling [`inotify_init1`]. 38 | /// 39 | /// This method passes both flags accepted by [`inotify_init1`], not giving 40 | /// the user any choice in the matter, as not passing the flags would be 41 | /// inappropriate in the context of this wrapper: 42 | /// 43 | /// - [`IN_CLOEXEC`] prevents leaking file descriptors to other processes. 44 | /// - [`IN_NONBLOCK`] controls the blocking behavior of the inotify API, 45 | /// which is entirely managed by this wrapper. 46 | /// 47 | /// # Errors 48 | /// 49 | /// Directly returns the error from the call to [`inotify_init1`], without 50 | /// adding any error conditions of its own. 51 | /// 52 | /// # Examples 53 | /// 54 | /// ``` 55 | /// use inotify::Inotify; 56 | /// 57 | /// let inotify = Inotify::init() 58 | /// .expect("Failed to initialize an inotify instance"); 59 | /// ``` 60 | /// 61 | /// [`inotify_init1`]: inotify_sys::inotify_init1 62 | /// [`IN_CLOEXEC`]: inotify_sys::IN_CLOEXEC 63 | /// [`IN_NONBLOCK`]: inotify_sys::IN_NONBLOCK 64 | pub fn init() -> io::Result { 65 | let fd = unsafe { 66 | // Initialize inotify and pass both `IN_CLOEXEC` and `IN_NONBLOCK`. 67 | // 68 | // `IN_NONBLOCK` is needed, because `Inotify` manages blocking 69 | // behavior for the API consumer, and the way we do that is to make 70 | // everything non-blocking by default and later override that as 71 | // required. 72 | // 73 | // Passing `IN_CLOEXEC` prevents leaking file descriptors to 74 | // processes executed by this process and seems to be a best 75 | // practice. I don't grasp this issue completely and failed to find 76 | // any authoritative sources on the topic. There's some discussion in 77 | // the open(2) and fcntl(2) man pages, but I didn't find that 78 | // helpful in understanding the issue of leaked file descriptors. 79 | // For what it's worth, there's a Rust issue about this: 80 | // https://github.com/rust-lang/rust/issues/12148 81 | ffi::inotify_init1(ffi::IN_CLOEXEC | ffi::IN_NONBLOCK) 82 | }; 83 | 84 | if fd == -1 { 85 | return Err(io::Error::last_os_error()); 86 | } 87 | 88 | Ok(Inotify { 89 | fd: Arc::new(FdGuard { 90 | fd, 91 | close_on_drop: AtomicBool::new(true), 92 | }), 93 | }) 94 | } 95 | 96 | /// Gets an interface that allows adding and removing watches. 97 | /// See [`Watches::add`] and [`Watches::remove`]. 98 | pub fn watches(&self) -> Watches { 99 | Watches::new(self.fd.clone()) 100 | } 101 | 102 | /// Deprecated: use `Inotify.watches().add()` instead 103 | #[deprecated = "use `Inotify.watches().add()` instead"] 104 | pub fn add_watch

(&mut self, path: P, mask: WatchMask) -> io::Result 105 | where 106 | P: AsRef, 107 | { 108 | self.watches().add(path, mask) 109 | } 110 | 111 | /// Deprecated: use `Inotify.watches().remove()` instead 112 | #[deprecated = "use `Inotify.watches().remove()` instead"] 113 | pub fn rm_watch(&mut self, wd: WatchDescriptor) -> io::Result<()> { 114 | self.watches().remove(wd) 115 | } 116 | 117 | /// Waits until events are available, then returns them 118 | /// 119 | /// Blocks the current thread until at least one event is available. If this 120 | /// is not desirable, please consider [`Inotify::read_events`]. 121 | /// 122 | /// This method calls [`Inotify::read_events`] internally and behaves 123 | /// essentially the same, apart from the blocking behavior. Please refer to 124 | /// the documentation of [`Inotify::read_events`] for more information. 125 | pub fn read_events_blocking<'a>(&mut self, buffer: &'a mut [u8]) -> io::Result> { 126 | unsafe { 127 | let res = fcntl(**self.fd, F_GETFL); 128 | if res == -1 { 129 | return Err(io::Error::last_os_error()); 130 | } 131 | if fcntl(**self.fd, F_SETFL, res & !O_NONBLOCK) == -1 { 132 | return Err(io::Error::last_os_error()); 133 | } 134 | }; 135 | let result = self.read_events(buffer); 136 | unsafe { 137 | let res = fcntl(**self.fd, F_GETFL); 138 | if res == -1 { 139 | return Err(io::Error::last_os_error()); 140 | } 141 | if fcntl(**self.fd, F_SETFL, res | O_NONBLOCK) == -1 { 142 | return Err(io::Error::last_os_error()); 143 | } 144 | }; 145 | 146 | result 147 | } 148 | 149 | /// Returns one buffer's worth of available events 150 | /// 151 | /// Reads as many events as possible into `buffer`, and returns an iterator 152 | /// over them. If no events are available, an iterator is still returned. If 153 | /// you need a method that will block until at least one event is available, 154 | /// please consider [`read_events_blocking`]. 155 | /// 156 | /// Please note that inotify will merge identical successive unread events 157 | /// into a single event. This means this method can not be used to count the 158 | /// number of file system events. 159 | /// 160 | /// The `buffer` argument, as the name indicates, is used as a buffer for 161 | /// the inotify events. Its contents may be overwritten. 162 | /// 163 | /// # Errors 164 | /// 165 | /// This function directly returns all errors from the call to [`read`]. 166 | /// In addition, [`ErrorKind::UnexpectedEof`] is returned, if the call to 167 | /// [`read`] returns `0`, signaling end-of-file. 168 | /// 169 | /// If `buffer` is too small, this will result in an error with 170 | /// [`ErrorKind::InvalidInput`]. On very old Linux kernels, 171 | /// [`ErrorKind::UnexpectedEof`] will be returned instead. 172 | /// 173 | /// # Examples 174 | /// 175 | /// ```no_run 176 | /// use inotify::Inotify; 177 | /// use std::io::ErrorKind; 178 | /// 179 | /// let mut inotify = Inotify::init() 180 | /// .expect("Failed to initialize an inotify instance"); 181 | /// 182 | /// let mut buffer = [0; 1024]; 183 | /// let events = loop { 184 | /// match inotify.read_events(&mut buffer) { 185 | /// Ok(events) => break events, 186 | /// Err(error) if error.kind() == ErrorKind::WouldBlock => continue, 187 | /// _ => panic!("Error while reading events"), 188 | /// } 189 | /// }; 190 | /// 191 | /// for event in events { 192 | /// // Handle event 193 | /// } 194 | /// ``` 195 | /// 196 | /// [`read_events_blocking`]: Self::read_events_blocking 197 | /// [`read`]: libc::read 198 | /// [`ErrorKind::UnexpectedEof`]: std::io::ErrorKind::UnexpectedEof 199 | /// [`ErrorKind::InvalidInput`]: std::io::ErrorKind::InvalidInput 200 | pub fn read_events<'a>(&mut self, buffer: &'a mut [u8]) -> io::Result> { 201 | let num_bytes = read_into_buffer(**self.fd, buffer); 202 | 203 | let num_bytes = match num_bytes { 204 | 0 => { 205 | return Err(io::Error::new( 206 | io::ErrorKind::UnexpectedEof, 207 | "`read` return `0`, signaling end-of-file", 208 | )); 209 | } 210 | -1 => { 211 | let error = io::Error::last_os_error(); 212 | return Err(error); 213 | } 214 | _ if num_bytes < 0 => { 215 | panic!( 216 | "{} {} {} {} {} {}", 217 | "Unexpected return value from `read`. Received a negative", 218 | "value that was not `-1`. According to the `read` man page", 219 | "this shouldn't happen, as either `-1` is returned on", 220 | "error, `0` on end-of-file, or a positive value for the", 221 | "number of bytes read. Returned value:", 222 | num_bytes, 223 | ); 224 | } 225 | _ => { 226 | // The value returned by `read` should be `isize`. Let's quickly 227 | // verify this with the following assignment, so we can be sure 228 | // our cast below is valid. 229 | let num_bytes: isize = num_bytes; 230 | 231 | // The type returned by `read` is `isize`, and we've ruled out 232 | // all negative values with the match arms above. This means we 233 | // can safely cast to `usize`. 234 | debug_assert!(num_bytes > 0); 235 | num_bytes as usize 236 | } 237 | }; 238 | 239 | Ok(Events::new(Arc::downgrade(&self.fd), buffer, num_bytes)) 240 | } 241 | 242 | /// Deprecated: use `into_event_stream()` instead, which enforces a single `Stream` and predictable reads. 243 | /// Using this method to create multiple `EventStream` instances from one `Inotify` is unsupported, 244 | /// as they will contend over one event source and each produce unpredictable stream contents. 245 | #[deprecated = "use `into_event_stream()` instead, which enforces a single Stream and predictable reads"] 246 | #[cfg(feature = "stream")] 247 | pub fn event_stream(&mut self, buffer: T) -> io::Result> 248 | where 249 | T: AsMut<[u8]> + AsRef<[u8]>, 250 | { 251 | EventStream::new(self.fd.clone(), buffer) 252 | } 253 | 254 | /// Create a stream which collects events. Consumes the `Inotify` instance. 255 | /// 256 | /// Returns a asynchronous `Stream` over the Inotify instance's events. 257 | #[cfg(feature = "stream")] 258 | pub fn into_event_stream(self, buffer: T) -> io::Result> 259 | where 260 | T: AsMut<[u8]> + AsRef<[u8]>, 261 | { 262 | EventStream::new(self.fd, buffer) 263 | } 264 | 265 | /// Creates an `Inotify` instance using the file descriptor which was originally 266 | /// initialized in `Inotify::init`. This is intended to be used to transform an 267 | /// `EventStream` back into an `Inotify`. Do not attempt to clone `Inotify` with this. 268 | #[cfg(feature = "stream")] 269 | pub(crate) fn from_file_descriptor(fd: Arc) -> Self { 270 | Inotify { fd } 271 | } 272 | 273 | /// Closes the inotify instance 274 | /// 275 | /// Closes the file descriptor referring to the inotify instance. The user 276 | /// usually doesn't have to call this function, as the underlying inotify 277 | /// instance is closed automatically, when [`Inotify`] is dropped. 278 | /// 279 | /// # Errors 280 | /// 281 | /// Directly returns the error from the call to [`close`], without adding any 282 | /// error conditions of its own. 283 | /// 284 | /// # Examples 285 | /// 286 | /// ``` 287 | /// use inotify::Inotify; 288 | /// 289 | /// let mut inotify = Inotify::init() 290 | /// .expect("Failed to initialize an inotify instance"); 291 | /// 292 | /// inotify.close() 293 | /// .expect("Failed to close inotify instance"); 294 | /// ``` 295 | /// 296 | /// [`close`]: libc::close 297 | pub fn close(self) -> io::Result<()> { 298 | // `self` will be dropped when this method returns. If this is the only 299 | // owner of `fd`, the `Arc` will also be dropped. The `Drop` 300 | // implementation for `FdGuard` will attempt to close the file descriptor 301 | // again, unless this flag here is cleared. 302 | self.fd.should_not_close(); 303 | 304 | match unsafe { ffi::close(**self.fd) } { 305 | 0 => Ok(()), 306 | _ => Err(io::Error::last_os_error()), 307 | } 308 | } 309 | } 310 | 311 | impl AsRawFd for Inotify { 312 | #[inline] 313 | fn as_raw_fd(&self) -> RawFd { 314 | self.fd.as_raw_fd() 315 | } 316 | } 317 | 318 | impl FromRawFd for Inotify { 319 | unsafe fn from_raw_fd(fd: RawFd) -> Self { 320 | Inotify { 321 | fd: Arc::new(FdGuard::from_raw_fd(fd)), 322 | } 323 | } 324 | } 325 | 326 | impl IntoRawFd for Inotify { 327 | #[inline] 328 | fn into_raw_fd(self) -> RawFd { 329 | self.fd.should_not_close(); 330 | self.fd.fd 331 | } 332 | } 333 | 334 | impl AsFd for Inotify { 335 | #[inline] 336 | fn as_fd(&self) -> BorrowedFd<'_> { 337 | self.fd.as_fd() 338 | } 339 | } 340 | 341 | impl From for OwnedFd { 342 | fn from(fd: Inotify) -> OwnedFd { 343 | unsafe { OwnedFd::from_raw_fd(fd.into_raw_fd()) } 344 | } 345 | } 346 | 347 | impl From for Inotify { 348 | fn from(fd: OwnedFd) -> Inotify { 349 | unsafe { Inotify::from_raw_fd(fd.into_raw_fd()) } 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Idiomatic inotify wrapper for the Rust programming language 2 | //! 3 | //! # About 4 | //! 5 | //! [inotify-rs] is an idiomatic wrapper around the Linux kernel's [inotify] API 6 | //! for the Rust programming language. It can be used for monitoring changes to 7 | //! files or directories. 8 | //! 9 | //! The [`Inotify`] struct is the main entry point into the API. 10 | //! The [`EventStream`] struct is designed to be used with async streams. 11 | //! 12 | //! # Examples 13 | //! 14 | //! If you just want to synchronously retrieve events 15 | //! ``` 16 | //! use inotify::{ 17 | //! Inotify, 18 | //! WatchMask, 19 | //! }; 20 | //! 21 | //! let mut inotify = Inotify::init() 22 | //! .expect("Error while initializing inotify instance"); 23 | //! 24 | //! # // Create a temporary file, so `Watches::add` won't return an error. 25 | //! # use std::fs::File; 26 | //! # let mut test_file = File::create("/tmp/inotify-rs-test-file") 27 | //! # .expect("Failed to create test file"); 28 | //! # 29 | //! // Watch for modify and close events. 30 | //! inotify 31 | //! .watches() 32 | //! .add( 33 | //! "/tmp/inotify-rs-test-file", 34 | //! WatchMask::MODIFY | WatchMask::CLOSE, 35 | //! ) 36 | //! .expect("Failed to add file watch"); 37 | //! 38 | //! # // Modify file, so the following `read_events_blocking` won't block. 39 | //! # use std::io::Write; 40 | //! # write!(&mut test_file, "something\n") 41 | //! # .expect("Failed to write something to test file"); 42 | //! # 43 | //! // Read events that were added with `Watches::add` above. 44 | //! let mut buffer = [0; 1024]; 45 | //! let events = inotify.read_events_blocking(&mut buffer) 46 | //! .expect("Error while reading events"); 47 | //! 48 | //! for event in events { 49 | //! // Handle event 50 | //! } 51 | //! ``` 52 | //! When you want to read events asynchronously, you need to convert it to [`EventStream`]. 53 | //! The transform function is [`Inotify::into_event_stream`] 54 | //! ``` 55 | //! # async fn stream_events() { 56 | //! # use futures_util::StreamExt; 57 | //! # 58 | //! # let mut inotify = inotify::Inotify::init() 59 | //! # .expect("Error while initializing inotify instance"); 60 | //! # 61 | //! let mut buffer = [0; 1024]; 62 | //! let mut stream = inotify.into_event_stream(&mut buffer) 63 | //! .expect("Error converting to stream"); 64 | //! 65 | //! // Read events from async stream 66 | //! while let Some(event_or_error) = stream.next().await { 67 | //! println!("event: {:?}", event_or_error.expect("Stream error")); 68 | //! } 69 | //! # } 70 | //! ``` 71 | //! # Attention: inotify gotchas 72 | //! 73 | //! inotify (as in, the Linux API, not this wrapper) has many edge cases, making 74 | //! it hard to use correctly. This can lead to weird and hard to find bugs in 75 | //! applications that are based on it. inotify-rs does its best to fix these 76 | //! issues, but sometimes this would require an amount of runtime overhead that 77 | //! is just unacceptable for a low-level wrapper such as this. 78 | //! 79 | //! We've documented any issues that inotify-rs has inherited from inotify, as 80 | //! far as we are aware of them. Please watch out for any further warnings 81 | //! throughout this documentation. If you want to be on the safe side, in case 82 | //! we have missed something, please read the [inotify man pages] carefully. 83 | //! 84 | //! [inotify-rs]: https://crates.io/crates/inotify 85 | //! [inotify]: https://en.wikipedia.org/wiki/Inotify 86 | //! [inotify man pages]: http://man7.org/linux/man-pages/man7/inotify.7.html 87 | 88 | #![deny(missing_docs)] 89 | #![deny(warnings)] 90 | #![deny(missing_debug_implementations)] 91 | 92 | #[macro_use] 93 | extern crate bitflags; 94 | 95 | mod events; 96 | mod fd_guard; 97 | mod inotify; 98 | mod util; 99 | mod watches; 100 | 101 | #[cfg(feature = "stream")] 102 | mod stream; 103 | 104 | pub use crate::events::{ 105 | Event, EventAuxiliaryFlags, EventKind, EventMask, EventMaskParseError, EventOwned, Events, 106 | ParsedEventMask, 107 | }; 108 | pub use crate::inotify::Inotify; 109 | pub use crate::util::{get_absolute_path_buffer_size, get_buffer_size}; 110 | pub use crate::watches::{WatchDescriptor, WatchMask, Watches}; 111 | 112 | #[cfg(feature = "stream")] 113 | pub use self::stream::EventStream; 114 | -------------------------------------------------------------------------------- /src/stream.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | os::unix::io::AsRawFd, 4 | pin::Pin, 5 | sync::Arc, 6 | task::{Context, Poll}, 7 | }; 8 | 9 | use futures_core::{ready, Stream}; 10 | use tokio::io::unix::AsyncFd; 11 | 12 | use crate::events::{Event, EventOwned}; 13 | use crate::fd_guard::FdGuard; 14 | use crate::util::read_into_buffer; 15 | use crate::watches::Watches; 16 | use crate::Inotify; 17 | 18 | /// Stream of inotify events 19 | /// 20 | /// Allows for streaming events returned by [`Inotify::into_event_stream`]. 21 | #[derive(Debug)] 22 | pub struct EventStream { 23 | fd: AsyncFd>, 24 | buffer: T, 25 | buffer_pos: usize, 26 | unused_bytes: usize, 27 | } 28 | 29 | impl EventStream 30 | where 31 | T: AsMut<[u8]> + AsRef<[u8]>, 32 | { 33 | /// Returns a new `EventStream` associated with the default reactor. 34 | pub(crate) fn new(fd: Arc, buffer: T) -> io::Result { 35 | Ok(EventStream { 36 | fd: AsyncFd::new(fd)?, 37 | buffer, 38 | buffer_pos: 0, 39 | unused_bytes: 0, 40 | }) 41 | } 42 | 43 | /// Returns an instance of `Watches` to add and remove watches. 44 | /// See [`Watches::add`] and [`Watches::remove`]. 45 | pub fn watches(&self) -> Watches { 46 | Watches::new(self.fd.get_ref().clone()) 47 | } 48 | 49 | /// Consumes the `EventStream` instance and returns an `Inotify` using the original 50 | /// file descriptor that was passed from `Inotify` to create the `EventStream`. 51 | pub fn into_inotify(self) -> Inotify { 52 | Inotify::from_file_descriptor(self.fd.into_inner()) 53 | } 54 | } 55 | 56 | impl Stream for EventStream 57 | where 58 | T: AsMut<[u8]> + AsRef<[u8]>, 59 | { 60 | type Item = io::Result; 61 | 62 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 63 | // Safety: safe because we never move out of `self_`. 64 | let self_ = unsafe { self.get_unchecked_mut() }; 65 | 66 | if self_.unused_bytes == 0 { 67 | // Nothing usable in buffer. Need to reset and fill buffer. 68 | self_.buffer_pos = 0; 69 | self_.unused_bytes = ready!(read(&self_.fd, self_.buffer.as_mut(), cx))?; 70 | } 71 | 72 | if self_.unused_bytes == 0 { 73 | // The previous read returned `0` signalling end-of-file. Let's 74 | // signal end-of-stream to the caller. 75 | return Poll::Ready(None); 76 | } 77 | 78 | // We have bytes in the buffer. inotify doesn't put partial events in 79 | // there, and we only take complete events out. That means we have at 80 | // least one event in there and can call `from_buffer` to take it out. 81 | let (bytes_consumed, event) = Event::from_buffer( 82 | Arc::downgrade(self_.fd.get_ref()), 83 | &self_.buffer.as_ref()[self_.buffer_pos..], 84 | ); 85 | self_.buffer_pos += bytes_consumed; 86 | self_.unused_bytes -= bytes_consumed; 87 | 88 | Poll::Ready(Some(Ok(event.to_owned()))) 89 | } 90 | } 91 | 92 | fn read( 93 | fd: &AsyncFd>, 94 | buffer: &mut [u8], 95 | cx: &mut Context, 96 | ) -> Poll> { 97 | let mut guard = ready!(fd.poll_read_ready(cx))?; 98 | let result = guard.try_io(|_| { 99 | let read = read_into_buffer(fd.as_raw_fd(), buffer); 100 | if read == -1 { 101 | return Err(io::Error::last_os_error()); 102 | } 103 | 104 | Ok(read as usize) 105 | }); 106 | 107 | match result { 108 | Ok(result) => Poll::Ready(result), 109 | Err(_would_block) => { 110 | cx.waker().wake_by_ref(); 111 | Poll::Pending 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::{io, mem, os::unix::io::RawFd, path::Path}; 2 | 3 | use inotify_sys as ffi; 4 | use libc::{c_void, size_t}; 5 | 6 | const INOTIFY_EVENT_SIZE: usize = mem::size_of::() + 257; 7 | 8 | pub fn read_into_buffer(fd: RawFd, buffer: &mut [u8]) -> isize { 9 | unsafe { 10 | ffi::read( 11 | fd, 12 | buffer.as_mut_ptr() as *mut c_void, 13 | buffer.len() as size_t, 14 | ) 15 | } 16 | } 17 | 18 | /// Get the inotify event buffer size 19 | /// 20 | /// The maximum size of an inotify event and thus the buffer size to hold it 21 | /// can be calculated using this formula: 22 | /// `sizeof(struct inotify_event) + NAME_MAX + 1` 23 | /// 24 | /// See: 25 | /// 26 | /// The NAME_MAX size formula is: 27 | /// `ABSOLUTE_PARENT_PATH_LEN + 1 + 255` 28 | /// 29 | /// - `ABSOLUTE_PARENT_PATH_LEN` will be calculated at runtime. 30 | /// - Add 1 to account for a `/`, either in between the parent path and a filename or for the root directory. 31 | /// - Add the maximum number of chars in a filename, 255. 32 | /// 33 | /// See: 34 | /// 35 | /// Unfortunately, we can't just do the same with max path length itself. 36 | /// 37 | /// See: 38 | /// 39 | /// This function is really just a fallible wrapper around `get_absolute_path_buffer_size()`. 40 | /// 41 | /// path: A relative or absolute path for the inotify events. 42 | pub fn get_buffer_size(path: &Path) -> io::Result { 43 | Ok(get_absolute_path_buffer_size(&path.canonicalize()?)) 44 | } 45 | 46 | /// Get the inotify event buffer size for an absolute path 47 | /// 48 | /// For relative paths, consider using `get_buffer_size()` which provides a fallible wrapper 49 | /// for this function. 50 | /// 51 | /// path: An absolute path for the inotify events. 52 | pub fn get_absolute_path_buffer_size(path: &Path) -> usize { 53 | // Get the length of the absolute parent path, if the path is not the root directory. 54 | // Because we canonicalize the path, we do not need to worry about prefixes. 55 | let parent_path_len = path 56 | .parent() 57 | .map(|parent_path| parent_path.as_os_str().len()) 58 | .unwrap_or(0); 59 | 60 | INOTIFY_EVENT_SIZE + parent_path_len 61 | } 62 | -------------------------------------------------------------------------------- /src/watches.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cmp::Ordering, 3 | ffi::CString, 4 | hash::{Hash, Hasher}, 5 | io, 6 | os::raw::c_int, 7 | os::unix::ffi::OsStrExt, 8 | path::Path, 9 | sync::{Arc, Weak}, 10 | }; 11 | 12 | use inotify_sys as ffi; 13 | 14 | use crate::fd_guard::FdGuard; 15 | 16 | bitflags! { 17 | /// Describes a file system watch 18 | /// 19 | /// Passed to [`Watches::add`], to describe what file system events 20 | /// to watch for, and how to do that. 21 | /// 22 | /// # Examples 23 | /// 24 | /// `WatchMask` constants can be passed to [`Watches::add`] as is. For 25 | /// example, here's how to create a watch that triggers an event when a file 26 | /// is accessed: 27 | /// 28 | /// ``` rust 29 | /// # use inotify::{ 30 | /// # Inotify, 31 | /// # WatchMask, 32 | /// # }; 33 | /// # 34 | /// # let mut inotify = Inotify::init().unwrap(); 35 | /// # 36 | /// # // Create a temporary file, so `Watches::add` won't return an error. 37 | /// # use std::fs::File; 38 | /// # File::create("/tmp/inotify-rs-test-file") 39 | /// # .expect("Failed to create test file"); 40 | /// # 41 | /// inotify.watches().add("/tmp/inotify-rs-test-file", WatchMask::ACCESS) 42 | /// .expect("Error adding watch"); 43 | /// ``` 44 | /// 45 | /// You can also combine multiple `WatchMask` constants. Here we add a watch 46 | /// this is triggered both when files are created or deleted in a directory: 47 | /// 48 | /// ``` rust 49 | /// # use inotify::{ 50 | /// # Inotify, 51 | /// # WatchMask, 52 | /// # }; 53 | /// # 54 | /// # let mut inotify = Inotify::init().unwrap(); 55 | /// inotify.watches().add("/tmp/", WatchMask::CREATE | WatchMask::DELETE) 56 | /// .expect("Error adding watch"); 57 | /// ``` 58 | #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)] 59 | pub struct WatchMask: u32 { 60 | /// File was accessed 61 | /// 62 | /// When watching a directory, this event is only triggered for objects 63 | /// inside the directory, not the directory itself. 64 | /// 65 | /// See [`inotify_sys::IN_ACCESS`]. 66 | const ACCESS = ffi::IN_ACCESS; 67 | 68 | /// Metadata (permissions, timestamps, ...) changed 69 | /// 70 | /// When watching a directory, this event can be triggered for the 71 | /// directory itself, as well as objects inside the directory. 72 | /// 73 | /// See [`inotify_sys::IN_ATTRIB`]. 74 | const ATTRIB = ffi::IN_ATTRIB; 75 | 76 | /// File opened for writing was closed 77 | /// 78 | /// When watching a directory, this event is only triggered for objects 79 | /// inside the directory, not the directory itself. 80 | /// 81 | /// See [`inotify_sys::IN_CLOSE_WRITE`]. 82 | const CLOSE_WRITE = ffi::IN_CLOSE_WRITE; 83 | 84 | /// File or directory not opened for writing was closed 85 | /// 86 | /// When watching a directory, this event can be triggered for the 87 | /// directory itself, as well as objects inside the directory. 88 | /// 89 | /// See [`inotify_sys::IN_CLOSE_NOWRITE`]. 90 | const CLOSE_NOWRITE = ffi::IN_CLOSE_NOWRITE; 91 | 92 | /// File/directory created in watched directory 93 | /// 94 | /// When watching a directory, this event is only triggered for objects 95 | /// inside the directory, not the directory itself. 96 | /// 97 | /// See [`inotify_sys::IN_CREATE`]. 98 | const CREATE = ffi::IN_CREATE; 99 | 100 | /// File/directory deleted from watched directory 101 | /// 102 | /// When watching a directory, this event is only triggered for objects 103 | /// inside the directory, not the directory itself. 104 | /// 105 | /// See [`inotify_sys::IN_DELETE`]. 106 | const DELETE = ffi::IN_DELETE; 107 | 108 | /// Watched file/directory was deleted 109 | /// 110 | /// See [`inotify_sys::IN_DELETE_SELF`]. 111 | const DELETE_SELF = ffi::IN_DELETE_SELF; 112 | 113 | /// File was modified 114 | /// 115 | /// When watching a directory, this event is only triggered for objects 116 | /// inside the directory, not the directory itself. 117 | /// 118 | /// See [`inotify_sys::IN_MODIFY`]. 119 | const MODIFY = ffi::IN_MODIFY; 120 | 121 | /// Watched file/directory was moved 122 | /// 123 | /// See [`inotify_sys::IN_MOVE_SELF`]. 124 | const MOVE_SELF = ffi::IN_MOVE_SELF; 125 | 126 | /// File was renamed/moved; watched directory contained old name 127 | /// 128 | /// When watching a directory, this event is only triggered for objects 129 | /// inside the directory, not the directory itself. 130 | /// 131 | /// See [`inotify_sys::IN_MOVED_FROM`]. 132 | const MOVED_FROM = ffi::IN_MOVED_FROM; 133 | 134 | /// File was renamed/moved; watched directory contains new name 135 | /// 136 | /// When watching a directory, this event is only triggered for objects 137 | /// inside the directory, not the directory itself. 138 | /// 139 | /// See [`inotify_sys::IN_MOVED_TO`]. 140 | const MOVED_TO = ffi::IN_MOVED_TO; 141 | 142 | /// File or directory was opened 143 | /// 144 | /// When watching a directory, this event can be triggered for the 145 | /// directory itself, as well as objects inside the directory. 146 | /// 147 | /// See [`inotify_sys::IN_OPEN`]. 148 | const OPEN = ffi::IN_OPEN; 149 | 150 | /// Watch for all events 151 | /// 152 | /// This constant is simply a convenient combination of the following 153 | /// other constants: 154 | /// 155 | /// - [`ACCESS`](Self::ACCESS) 156 | /// - [`ATTRIB`](Self::ATTRIB) 157 | /// - [`CLOSE_WRITE`](Self::CLOSE_WRITE) 158 | /// - [`CLOSE_NOWRITE`](Self::CLOSE_NOWRITE) 159 | /// - [`CREATE`](Self::CREATE) 160 | /// - [`DELETE`](Self::DELETE) 161 | /// - [`DELETE_SELF`](Self::DELETE_SELF) 162 | /// - [`MODIFY`](Self::MODIFY) 163 | /// - [`MOVE_SELF`](Self::MOVE_SELF) 164 | /// - [`MOVED_FROM`](Self::MOVED_FROM) 165 | /// - [`MOVED_TO`](Self::MOVED_TO) 166 | /// - [`OPEN`](Self::OPEN) 167 | /// 168 | /// See [`inotify_sys::IN_ALL_EVENTS`]. 169 | const ALL_EVENTS = ffi::IN_ALL_EVENTS; 170 | 171 | /// Watch for all move events 172 | /// 173 | /// This constant is simply a convenient combination of the following 174 | /// other constants: 175 | /// 176 | /// - [`MOVED_FROM`](Self::MOVED_FROM) 177 | /// - [`MOVED_TO`](Self::MOVED_TO) 178 | /// 179 | /// See [`inotify_sys::IN_MOVE`]. 180 | const MOVE = ffi::IN_MOVE; 181 | 182 | /// Watch for all close events 183 | /// 184 | /// This constant is simply a convenient combination of the following 185 | /// other constants: 186 | /// 187 | /// - [`CLOSE_WRITE`](Self::CLOSE_WRITE) 188 | /// - [`CLOSE_NOWRITE`](Self::CLOSE_NOWRITE) 189 | /// 190 | /// See [`inotify_sys::IN_CLOSE`]. 191 | const CLOSE = ffi::IN_CLOSE; 192 | 193 | /// Don't dereference the path if it is a symbolic link 194 | /// 195 | /// See [`inotify_sys::IN_DONT_FOLLOW`]. 196 | const DONT_FOLLOW = ffi::IN_DONT_FOLLOW; 197 | 198 | /// Filter events for directory entries that have been unlinked 199 | /// 200 | /// See [`inotify_sys::IN_EXCL_UNLINK`]. 201 | const EXCL_UNLINK = ffi::IN_EXCL_UNLINK; 202 | 203 | /// If a watch for the inode exists, amend it instead of replacing it 204 | /// 205 | /// See [`inotify_sys::IN_MASK_ADD`]. 206 | const MASK_ADD = ffi::IN_MASK_ADD; 207 | 208 | /// Only receive one event, then remove the watch 209 | /// 210 | /// See [`inotify_sys::IN_ONESHOT`]. 211 | const ONESHOT = ffi::IN_ONESHOT; 212 | 213 | /// Only watch path, if it is a directory 214 | /// 215 | /// See [`inotify_sys::IN_ONLYDIR`]. 216 | const ONLYDIR = ffi::IN_ONLYDIR; 217 | } 218 | } 219 | 220 | impl WatchMask { 221 | /// Wrapper around [`Self::from_bits_retain`] for backwards compatibility 222 | /// 223 | /// # Safety 224 | /// 225 | /// This function is not actually unsafe. It is just a wrapper around the 226 | /// safe [`Self::from_bits_retain`]. 227 | #[deprecated = "Use the safe `from_bits_retain` method instead"] 228 | pub unsafe fn from_bits_unchecked(bits: u32) -> Self { 229 | Self::from_bits_retain(bits) 230 | } 231 | } 232 | 233 | impl WatchDescriptor { 234 | /// Getter method for a watcher's id. 235 | /// 236 | /// Can be used to distinguish events for files with the same name. 237 | pub fn get_watch_descriptor_id(&self) -> c_int { 238 | self.id 239 | } 240 | } 241 | 242 | /// Interface for adding and removing watches 243 | #[derive(Clone, Debug)] 244 | pub struct Watches { 245 | pub(crate) fd: Arc, 246 | } 247 | 248 | impl Watches { 249 | /// Init watches with an inotify file descriptor 250 | pub(crate) fn new(fd: Arc) -> Self { 251 | Watches { fd } 252 | } 253 | 254 | /// Adds or updates a watch for the given path 255 | /// 256 | /// Adds a new watch or updates an existing one for the file referred to by 257 | /// `path`. Returns a watch descriptor that can be used to refer to this 258 | /// watch later. 259 | /// 260 | /// The `mask` argument defines what kind of changes the file should be 261 | /// watched for, and how to do that. See the documentation of [`WatchMask`] 262 | /// for details. 263 | /// 264 | /// If this method is used to add a new watch, a new [`WatchDescriptor`] is 265 | /// returned. If it is used to update an existing watch, a 266 | /// [`WatchDescriptor`] that equals the previously returned 267 | /// [`WatchDescriptor`] for that watch is returned instead. 268 | /// 269 | /// Under the hood, this method just calls [`inotify_add_watch`] and does 270 | /// some trivial translation between the types on the Rust side and the C 271 | /// side. 272 | /// 273 | /// # Attention: Updating watches and hardlinks 274 | /// 275 | /// As mentioned above, this method can be used to update an existing watch. 276 | /// This is usually done by calling this method with the same `path` 277 | /// argument that it has been called with before. But less obviously, it can 278 | /// also happen if the method is called with a different path that happens 279 | /// to link to the same inode. 280 | /// 281 | /// You can detect this by keeping track of [`WatchDescriptor`]s and the 282 | /// paths they have been returned for. If the same [`WatchDescriptor`] is 283 | /// returned for a different path (and you haven't freed the 284 | /// [`WatchDescriptor`] by removing the watch), you know you have two paths 285 | /// pointing to the same inode, being watched by the same watch. 286 | /// 287 | /// # Errors 288 | /// 289 | /// Directly returns the error from the call to 290 | /// [`inotify_add_watch`][`inotify_add_watch`] (translated into an 291 | /// `io::Error`), without adding any error conditions of 292 | /// its own. 293 | /// 294 | /// # Examples 295 | /// 296 | /// ``` 297 | /// use inotify::{ 298 | /// Inotify, 299 | /// WatchMask, 300 | /// }; 301 | /// 302 | /// let mut inotify = Inotify::init() 303 | /// .expect("Failed to initialize an inotify instance"); 304 | /// 305 | /// # // Create a temporary file, so `Watches::add` won't return an error. 306 | /// # use std::fs::File; 307 | /// # File::create("/tmp/inotify-rs-test-file") 308 | /// # .expect("Failed to create test file"); 309 | /// # 310 | /// inotify.watches().add("/tmp/inotify-rs-test-file", WatchMask::MODIFY) 311 | /// .expect("Failed to add file watch"); 312 | /// 313 | /// // Handle events for the file here 314 | /// ``` 315 | /// 316 | /// [`inotify_add_watch`]: inotify_sys::inotify_add_watch 317 | pub fn add

(&mut self, path: P, mask: WatchMask) -> io::Result 318 | where 319 | P: AsRef, 320 | { 321 | let path = CString::new(path.as_ref().as_os_str().as_bytes())?; 322 | 323 | let wd = 324 | unsafe { ffi::inotify_add_watch(**self.fd, path.as_ptr() as *const _, mask.bits()) }; 325 | 326 | match wd { 327 | -1 => Err(io::Error::last_os_error()), 328 | _ => Ok(WatchDescriptor { 329 | id: wd, 330 | fd: Arc::downgrade(&self.fd), 331 | }), 332 | } 333 | } 334 | 335 | /// Stops watching a file 336 | /// 337 | /// Removes the watch represented by the provided [`WatchDescriptor`] by 338 | /// calling [`inotify_rm_watch`]. [`WatchDescriptor`]s can be obtained via 339 | /// [`Watches::add`], or from the `wd` field of [`Event`]. 340 | /// 341 | /// # Errors 342 | /// 343 | /// Directly returns the error from the call to [`inotify_rm_watch`]. 344 | /// Returns an [`io::Error`] with [`ErrorKind`]`::InvalidInput`, if the given 345 | /// [`WatchDescriptor`] did not originate from this [`Inotify`] instance. 346 | /// 347 | /// # Examples 348 | /// 349 | /// ``` 350 | /// use inotify::Inotify; 351 | /// 352 | /// let mut inotify = Inotify::init() 353 | /// .expect("Failed to initialize an inotify instance"); 354 | /// 355 | /// # // Create a temporary file, so `Watches::add` won't return an error. 356 | /// # use std::fs::File; 357 | /// # let mut test_file = File::create("/tmp/inotify-rs-test-file") 358 | /// # .expect("Failed to create test file"); 359 | /// # 360 | /// # // Add a watch and modify the file, so the code below doesn't block 361 | /// # // forever. 362 | /// # use inotify::WatchMask; 363 | /// # inotify.watches().add("/tmp/inotify-rs-test-file", WatchMask::MODIFY) 364 | /// # .expect("Failed to add file watch"); 365 | /// # use std::io::Write; 366 | /// # write!(&mut test_file, "something\n") 367 | /// # .expect("Failed to write something to test file"); 368 | /// # 369 | /// let mut buffer = [0; 1024]; 370 | /// let events = inotify 371 | /// .read_events_blocking(&mut buffer) 372 | /// .expect("Error while waiting for events"); 373 | /// let mut watches = inotify.watches(); 374 | /// 375 | /// for event in events { 376 | /// watches.remove(event.wd); 377 | /// } 378 | /// ``` 379 | /// 380 | /// [`inotify_rm_watch`]: inotify_sys::inotify_rm_watch 381 | /// [`Event`]: crate::Event 382 | /// [`Inotify`]: crate::Inotify 383 | /// [`io::Error`]: std::io::Error 384 | /// [`ErrorKind`]: std::io::ErrorKind 385 | pub fn remove(&mut self, wd: WatchDescriptor) -> io::Result<()> { 386 | if wd.fd.upgrade().as_ref() != Some(&self.fd) { 387 | return Err(io::Error::new( 388 | io::ErrorKind::InvalidInput, 389 | "Invalid WatchDescriptor", 390 | )); 391 | } 392 | 393 | let result = unsafe { ffi::inotify_rm_watch(**self.fd, wd.id) }; 394 | match result { 395 | 0 => Ok(()), 396 | -1 => Err(io::Error::last_os_error()), 397 | _ => panic!("unexpected return code from inotify_rm_watch ({})", result), 398 | } 399 | } 400 | } 401 | 402 | /// Represents a watch on an inode 403 | /// 404 | /// Can be obtained from [`Watches::add`] or from an [`Event`]. A watch 405 | /// descriptor can be used to get inotify to stop watching an inode by passing 406 | /// it to [`Watches::remove`]. 407 | /// 408 | /// [`Event`]: crate::Event 409 | #[derive(Clone, Debug)] 410 | pub struct WatchDescriptor { 411 | pub(crate) id: c_int, 412 | pub(crate) fd: Weak, 413 | } 414 | 415 | impl Eq for WatchDescriptor {} 416 | 417 | impl PartialEq for WatchDescriptor { 418 | fn eq(&self, other: &Self) -> bool { 419 | let self_fd = self.fd.upgrade(); 420 | let other_fd = other.fd.upgrade(); 421 | 422 | self.id == other.id && self_fd.is_some() && self_fd == other_fd 423 | } 424 | } 425 | 426 | impl Ord for WatchDescriptor { 427 | fn cmp(&self, other: &Self) -> Ordering { 428 | self.id.cmp(&other.id) 429 | } 430 | } 431 | 432 | impl PartialOrd for WatchDescriptor { 433 | fn partial_cmp(&self, other: &Self) -> Option { 434 | Some(self.cmp(other)) 435 | } 436 | } 437 | 438 | impl Hash for WatchDescriptor { 439 | fn hash(&self, state: &mut H) { 440 | // This function only takes `self.id` into account, as `self.fd` is a 441 | // weak pointer that might no longer be available. Since neither 442 | // panicking nor changing the hash depending on whether it's available 443 | // is acceptable, we just don't look at it at all. 444 | // I don't think that this influences storage in a `HashMap` or 445 | // `HashSet` negatively, as storing `WatchDescriptor`s from different 446 | // `Inotify` instances seems like something of an anti-pattern anyway. 447 | self.id.hash(state); 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /tests/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | // This test suite is incomplete and doesn't cover all available functionality. 4 | // Contributions to improve test coverage would be highly appreciated! 5 | 6 | use inotify::{Inotify, WatchMask}; 7 | use std::fs::File; 8 | use std::io::{ErrorKind, Write}; 9 | use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd}; 10 | use std::path::PathBuf; 11 | use tempfile::TempDir; 12 | 13 | #[cfg(feature = "stream")] 14 | use futures_util::StreamExt; 15 | #[cfg(feature = "stream")] 16 | use inotify::EventMask; 17 | #[cfg(feature = "stream")] 18 | use maplit::hashmap; 19 | #[cfg(feature = "stream")] 20 | use rand::{prelude::SliceRandom, thread_rng}; 21 | #[cfg(feature = "stream")] 22 | use std::sync::{Arc, Mutex}; 23 | 24 | #[test] 25 | fn it_should_watch_a_file() { 26 | let mut testdir = TestDir::new(); 27 | let (path, mut file) = testdir.new_file(); 28 | 29 | let mut inotify = Inotify::init().unwrap(); 30 | let watch = inotify.watches().add(&path, WatchMask::MODIFY).unwrap(); 31 | 32 | write_to(&mut file); 33 | 34 | let mut buffer = [0; 1024]; 35 | let events = inotify.read_events_blocking(&mut buffer).unwrap(); 36 | 37 | let mut num_events = 0; 38 | for event in events { 39 | assert_eq!(watch, event.wd); 40 | num_events += 1; 41 | } 42 | assert!(num_events > 0); 43 | } 44 | 45 | #[cfg(feature = "stream")] 46 | #[tokio::test] 47 | async fn it_should_watch_a_file_async() { 48 | let mut testdir = TestDir::new(); 49 | let (path, mut file) = testdir.new_file(); 50 | 51 | let inotify = Inotify::init().unwrap(); 52 | 53 | // Hold ownership of `watches` for this test, so that the underlying file descriptor has 54 | // at least one reference to keep it alive, and we can inspect the WatchDescriptors below. 55 | // Otherwise the `Weak` contained in the WatchDescriptors will be invalidated 56 | // when `inotify` is consumed by `into_event_stream()` and the EventStream is dropped 57 | // during `await`. 58 | let mut watches = inotify.watches(); 59 | 60 | let watch = watches.add(&path, WatchMask::MODIFY).unwrap(); 61 | 62 | write_to(&mut file); 63 | 64 | let mut buffer = [0; 1024]; 65 | 66 | use futures_util::StreamExt; 67 | let events = inotify 68 | .into_event_stream(&mut buffer[..]) 69 | .unwrap() 70 | .take(1) 71 | .collect::>() 72 | .await; 73 | 74 | let mut num_events = 0; 75 | for event in events.into_iter().flatten() { 76 | assert_eq!(watch, event.wd); 77 | num_events += 1; 78 | } 79 | assert!(num_events > 0); 80 | } 81 | 82 | #[cfg(feature = "stream")] 83 | #[tokio::test] 84 | async fn it_should_watch_a_file_from_eventstream_watches() { 85 | let mut testdir = TestDir::new(); 86 | let (path, mut file) = testdir.new_file(); 87 | 88 | let inotify = Inotify::init().unwrap(); 89 | 90 | let mut buffer = [0; 1024]; 91 | 92 | use futures_util::StreamExt; 93 | let stream = inotify.into_event_stream(&mut buffer[..]).unwrap(); 94 | 95 | // Hold ownership of `watches` for this test, so that the underlying file descriptor has 96 | // at least one reference to keep it alive, and we can inspect the WatchDescriptors below. 97 | // Otherwise the `Weak` contained in the WatchDescriptors will be invalidated 98 | // when `stream` is dropped during `await`. 99 | let mut watches = stream.watches(); 100 | 101 | let watch = watches.add(&path, WatchMask::MODIFY).unwrap(); 102 | write_to(&mut file); 103 | 104 | let events = stream.take(1).collect::>().await; 105 | 106 | let mut num_events = 0; 107 | for event in events.into_iter().flatten() { 108 | assert_eq!(watch, event.wd); 109 | num_events += 1; 110 | } 111 | assert!(num_events > 0); 112 | } 113 | 114 | #[cfg(feature = "stream")] 115 | #[tokio::test] 116 | async fn it_should_watch_a_file_after_converting_back_from_eventstream() { 117 | let mut testdir = TestDir::new(); 118 | let (path, mut file) = testdir.new_file(); 119 | 120 | let inotify = Inotify::init().unwrap(); 121 | 122 | let mut buffer = [0; 1024]; 123 | let stream = inotify.into_event_stream(&mut buffer[..]).unwrap(); 124 | let mut inotify = stream.into_inotify(); 125 | 126 | let watch = inotify.watches().add(&path, WatchMask::MODIFY).unwrap(); 127 | 128 | write_to(&mut file); 129 | 130 | let events = inotify.read_events_blocking(&mut buffer).unwrap(); 131 | 132 | let mut num_events = 0; 133 | for event in events { 134 | assert_eq!(watch, event.wd); 135 | num_events += 1; 136 | } 137 | assert!(num_events > 0); 138 | } 139 | 140 | #[test] 141 | fn it_should_return_immediately_if_no_events_are_available() { 142 | let mut inotify = Inotify::init().unwrap(); 143 | 144 | let mut buffer = [0; 1024]; 145 | assert_eq!( 146 | inotify.read_events(&mut buffer).unwrap_err().kind(), 147 | ErrorKind::WouldBlock 148 | ); 149 | } 150 | 151 | #[test] 152 | fn it_should_convert_the_name_into_an_os_str() { 153 | let mut testdir = TestDir::new(); 154 | let (path, mut file) = testdir.new_file(); 155 | 156 | let mut inotify = Inotify::init().unwrap(); 157 | inotify 158 | .watches() 159 | .add(path.parent().unwrap(), WatchMask::MODIFY) 160 | .unwrap(); 161 | 162 | write_to(&mut file); 163 | 164 | let mut buffer = [0; 1024]; 165 | let mut events = inotify.read_events_blocking(&mut buffer).unwrap(); 166 | 167 | if let Some(event) = events.next() { 168 | assert_eq!(path.file_name(), event.name); 169 | } else { 170 | panic!("Expected inotify event"); 171 | } 172 | } 173 | 174 | #[test] 175 | fn it_should_set_name_to_none_if_it_is_empty() { 176 | let mut testdir = TestDir::new(); 177 | let (path, mut file) = testdir.new_file(); 178 | 179 | let mut inotify = Inotify::init().unwrap(); 180 | inotify.watches().add(&path, WatchMask::MODIFY).unwrap(); 181 | 182 | write_to(&mut file); 183 | 184 | let mut buffer = [0; 1024]; 185 | let mut events = inotify.read_events_blocking(&mut buffer).unwrap(); 186 | 187 | if let Some(event) = events.next() { 188 | assert_eq!(event.name, None); 189 | } else { 190 | panic!("Expected inotify event"); 191 | } 192 | } 193 | 194 | #[test] 195 | fn it_should_not_accept_watchdescriptors_from_other_instances() { 196 | let mut testdir = TestDir::new(); 197 | let (path, _) = testdir.new_file(); 198 | 199 | let inotify = Inotify::init().unwrap(); 200 | let _ = inotify.watches().add(&path, WatchMask::ACCESS).unwrap(); 201 | 202 | let second_inotify = Inotify::init().unwrap(); 203 | let wd2 = second_inotify 204 | .watches() 205 | .add(&path, WatchMask::ACCESS) 206 | .unwrap(); 207 | 208 | assert_eq!( 209 | inotify.watches().remove(wd2).unwrap_err().kind(), 210 | ErrorKind::InvalidInput 211 | ); 212 | } 213 | 214 | #[test] 215 | fn watch_descriptors_from_different_inotify_instances_should_not_be_equal() { 216 | let mut testdir = TestDir::new(); 217 | let (path, _) = testdir.new_file(); 218 | 219 | let inotify_1 = Inotify::init().unwrap(); 220 | let inotify_2 = Inotify::init().unwrap(); 221 | 222 | let wd_1 = inotify_1.watches().add(&path, WatchMask::ACCESS).unwrap(); 223 | let wd_2 = inotify_2.watches().add(&path, WatchMask::ACCESS).unwrap(); 224 | 225 | // As far as inotify is concerned, watch descriptors are just integers that 226 | // are scoped per inotify instance. This means that multiple instances will 227 | // produce the same watch descriptor number, a case we want inotify-rs to 228 | // detect. 229 | assert!(wd_1 != wd_2); 230 | } 231 | 232 | #[test] 233 | fn watch_descriptor_equality_should_not_be_confused_by_reused_fds() { 234 | let mut testdir = TestDir::new(); 235 | let (path, _) = testdir.new_file(); 236 | 237 | // When a new inotify instance is created directly after closing another 238 | // one, it is possible that the file descriptor is reused immediately, and 239 | // we end up with a new instance that has the same file descriptor as the 240 | // old one. 241 | // This is quite likely, but it doesn't happen every time. Therefore we may 242 | // need a few tries until we find two instances where that is the case. 243 | let (wd_1, inotify_2) = loop { 244 | let inotify_1 = Inotify::init().unwrap(); 245 | 246 | let wd_1 = inotify_1.watches().add(&path, WatchMask::ACCESS).unwrap(); 247 | let fd_1 = inotify_1.as_raw_fd(); 248 | 249 | inotify_1.close().unwrap(); 250 | let inotify_2 = Inotify::init().unwrap(); 251 | 252 | if fd_1 == inotify_2.as_raw_fd() { 253 | break (wd_1, inotify_2); 254 | } 255 | }; 256 | 257 | let wd_2 = inotify_2.watches().add(&path, WatchMask::ACCESS).unwrap(); 258 | 259 | // The way we engineered this situation, both `WatchDescriptor` instances 260 | // have the same fields. They still come from different inotify instances 261 | // though, so they shouldn't be equal. 262 | assert!(wd_1 != wd_2); 263 | 264 | inotify_2.close().unwrap(); 265 | 266 | // A little extra gotcha: If both inotify instances are closed, and the `Eq` 267 | // implementation naively compares the weak pointers, both will be `None`, 268 | // making them equal. Let's make sure this isn't the case. 269 | assert!(wd_1 != wd_2); 270 | } 271 | 272 | #[test] 273 | fn it_should_implement_raw_fd_traits_correctly() { 274 | let fd = Inotify::init() 275 | .expect("Failed to initialize inotify instance") 276 | .into_raw_fd(); 277 | 278 | // If `IntoRawFd` has been implemented naively, `Inotify`'s `Drop` 279 | // implementation will have closed the inotify instance at this point. Let's 280 | // make sure this didn't happen. 281 | let mut inotify = unsafe { ::from_raw_fd(fd) }; 282 | 283 | let mut buffer = [0; 1024]; 284 | if let Err(error) = inotify.read_events(&mut buffer) { 285 | if error.kind() != ErrorKind::WouldBlock { 286 | panic!("Failed to add watch: {}", error); 287 | } 288 | } 289 | } 290 | 291 | #[test] 292 | fn it_should_watch_correctly_with_a_watches_clone() { 293 | let mut testdir = TestDir::new(); 294 | let (path, mut file) = testdir.new_file(); 295 | 296 | let mut inotify = Inotify::init().unwrap(); 297 | let mut watches1 = inotify.watches(); 298 | let mut watches2 = watches1.clone(); 299 | let watch1 = watches1.add(&path, WatchMask::MODIFY).unwrap(); 300 | let watch2 = watches2.add(&path, WatchMask::MODIFY).unwrap(); 301 | 302 | // same path and same Inotify should return same descriptor 303 | assert_eq!(watch1, watch2); 304 | 305 | write_to(&mut file); 306 | 307 | let mut buffer = [0; 1024]; 308 | let events = inotify.read_events_blocking(&mut buffer).unwrap(); 309 | 310 | let mut num_events = 0; 311 | for event in events { 312 | assert_eq!(watch2, event.wd); 313 | num_events += 1; 314 | } 315 | assert!(num_events > 0); 316 | } 317 | 318 | #[cfg(feature = "stream")] 319 | #[tokio::test] 320 | /// Testing if two files with the same name but different directories 321 | /// (e.g. "file_a" and "another_dir/file_a") are distinguished when _randomly_ 322 | /// triggering a DELETE_SELF for the two files. 323 | async fn it_should_distinguish_event_for_files_with_same_name() { 324 | let mut testdir = TestDir::new(); 325 | let testdir_path = testdir.dir.path().to_owned(); 326 | let file_order = Arc::new(Mutex::new(vec!["file_a", "another_dir/file_a"])); 327 | file_order.lock().unwrap().shuffle(&mut thread_rng()); 328 | let file_order_clone = file_order.clone(); 329 | 330 | let inotify = Inotify::init().expect("Failed to initialize inotify instance"); 331 | 332 | // creating file_a inside `TestDir.dir` 333 | let (path_1, _) = testdir.new_file_with_name("file_a"); 334 | // creating a directory inside `TestDir.dir` 335 | testdir.new_directory_with_name("another_dir"); 336 | // creating a file inside `TestDir.dir/another_dir` 337 | let (path_2, _) = testdir.new_file_in_directory_with_name("another_dir", "file_a"); 338 | 339 | // watching both files for `DELETE_SELF` 340 | let wd_1 = inotify 341 | .watches() 342 | .add(&path_1, WatchMask::DELETE_SELF) 343 | .unwrap(); 344 | let wd_2 = inotify 345 | .watches() 346 | .add(&path_2, WatchMask::DELETE_SELF) 347 | .unwrap(); 348 | 349 | let expected_ids = hashmap! { 350 | wd_1.get_watch_descriptor_id() => "file_a", 351 | wd_2.get_watch_descriptor_id() => "another_dir/file_a" 352 | }; 353 | let mut buffer = [0; 1024]; 354 | 355 | let file_removal_handler = tokio::spawn(async move { 356 | for file in file_order.lock().unwrap().iter() { 357 | testdir.delete_file(file); 358 | } 359 | }); 360 | 361 | let event_handle = tokio::spawn(async move { 362 | let mut events = inotify.into_event_stream(&mut buffer).unwrap(); 363 | while let Some(Ok(event)) = events.next().await { 364 | if event.mask == EventMask::DELETE_SELF { 365 | let id = event.wd.get_watch_descriptor_id(); 366 | let file = expected_ids.get(&id).unwrap(); 367 | let full_path = testdir_path.join(*file); 368 | println!("file {:?} was deleted", full_path); 369 | file_order_clone.lock().unwrap().retain(|&x| x != *file); 370 | 371 | if file_order_clone.lock().unwrap().is_empty() { 372 | break; 373 | } 374 | } 375 | } 376 | }); 377 | 378 | let () = event_handle.await.unwrap(); 379 | let () = file_removal_handler.await.unwrap(); 380 | } 381 | 382 | struct TestDir { 383 | dir: TempDir, 384 | counter: u32, 385 | } 386 | 387 | impl TestDir { 388 | fn new() -> TestDir { 389 | TestDir { 390 | dir: TempDir::new().unwrap(), 391 | counter: 0, 392 | } 393 | } 394 | 395 | #[cfg(feature = "stream")] 396 | fn new_file_with_name(&mut self, file_name: &str) -> (PathBuf, File) { 397 | self.counter += 1; 398 | 399 | let path = self.dir.path().join(file_name); 400 | let file = File::create(&path) 401 | .unwrap_or_else(|error| panic!("Failed to create temporary file: {}", error)); 402 | 403 | (path, file) 404 | } 405 | 406 | #[cfg(feature = "stream")] 407 | fn delete_file(&mut self, relative_path_to_file: &str) { 408 | let path = &self.dir.path().join(relative_path_to_file); 409 | std::fs::remove_file(path).unwrap(); 410 | } 411 | 412 | #[cfg(feature = "stream")] 413 | fn new_file_in_directory_with_name( 414 | &mut self, 415 | dir_name: &str, 416 | file_name: &str, 417 | ) -> (PathBuf, File) { 418 | self.counter += 1; 419 | 420 | let path = self.dir.path().join(dir_name).join(file_name); 421 | let file = File::create(&path) 422 | .unwrap_or_else(|error| panic!("Failed to create temporary file: {}", error)); 423 | 424 | (path, file) 425 | } 426 | 427 | #[cfg(feature = "stream")] 428 | fn new_directory_with_name(&mut self, dir_name: &str) -> PathBuf { 429 | let path = self.dir.path().join(dir_name); 430 | let () = std::fs::create_dir(&path).unwrap(); 431 | path.to_path_buf() 432 | } 433 | 434 | fn new_file(&mut self) -> (PathBuf, File) { 435 | let id = self.counter; 436 | self.counter += 1; 437 | 438 | let path = self.dir.path().join("file-".to_string() + &id.to_string()); 439 | let file = File::create(&path) 440 | .unwrap_or_else(|error| panic!("Failed to create temporary file: {}", error)); 441 | 442 | (path, file) 443 | } 444 | } 445 | 446 | fn write_to(file: &mut File) { 447 | file.write_all(b"This should trigger an inotify event.") 448 | .unwrap_or_else(|error| panic!("Failed to write to file: {}", error)); 449 | } 450 | --------------------------------------------------------------------------------