├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── examples ├── config.yaml └── example.rs └── src ├── backend ├── inotify.rs ├── mod.rs └── notify.rs ├── inotify.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "async-stream" 16 | version = "0.3.5" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" 19 | dependencies = [ 20 | "async-stream-impl", 21 | "futures-core", 22 | "pin-project-lite", 23 | ] 24 | 25 | [[package]] 26 | name = "async-stream-impl" 27 | version = "0.3.5" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" 30 | dependencies = [ 31 | "proc-macro2", 32 | "quote", 33 | "syn 2.0.18", 34 | ] 35 | 36 | [[package]] 37 | name = "autocfg" 38 | version = "1.1.0" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 41 | 42 | [[package]] 43 | name = "bitflags" 44 | version = "1.3.2" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 47 | 48 | [[package]] 49 | name = "bitmask-enum" 50 | version = "2.1.0" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "fd9e32d7420c85055e8107e5b2463c4eeefeaac18b52359fe9f9c08a18f342b2" 53 | dependencies = [ 54 | "quote", 55 | "syn 1.0.109", 56 | ] 57 | 58 | [[package]] 59 | name = "bytes" 60 | version = "1.4.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" 63 | 64 | [[package]] 65 | name = "cc" 66 | version = "1.0.79" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 69 | 70 | [[package]] 71 | name = "cfg-if" 72 | version = "1.0.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 75 | 76 | [[package]] 77 | name = "crossbeam-channel" 78 | version = "0.5.8" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" 81 | dependencies = [ 82 | "cfg-if", 83 | "crossbeam-utils", 84 | ] 85 | 86 | [[package]] 87 | name = "crossbeam-utils" 88 | version = "0.8.16" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" 91 | dependencies = [ 92 | "cfg-if", 93 | ] 94 | 95 | [[package]] 96 | name = "env_logger" 97 | version = "0.10.0" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" 100 | dependencies = [ 101 | "humantime", 102 | "is-terminal", 103 | "log", 104 | "regex", 105 | "termcolor", 106 | ] 107 | 108 | [[package]] 109 | name = "errno" 110 | version = "0.3.1" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" 113 | dependencies = [ 114 | "errno-dragonfly", 115 | "libc", 116 | "windows-sys 0.48.0", 117 | ] 118 | 119 | [[package]] 120 | name = "errno-dragonfly" 121 | version = "0.1.2" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 124 | dependencies = [ 125 | "cc", 126 | "libc", 127 | ] 128 | 129 | [[package]] 130 | name = "filetime" 131 | version = "0.2.21" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" 134 | dependencies = [ 135 | "cfg-if", 136 | "libc", 137 | "redox_syscall 0.2.16", 138 | "windows-sys 0.48.0", 139 | ] 140 | 141 | [[package]] 142 | name = "fsevent-sys" 143 | version = "4.1.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" 146 | dependencies = [ 147 | "libc", 148 | ] 149 | 150 | [[package]] 151 | name = "futures" 152 | version = "0.3.28" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" 155 | dependencies = [ 156 | "futures-channel", 157 | "futures-core", 158 | "futures-executor", 159 | "futures-io", 160 | "futures-sink", 161 | "futures-task", 162 | "futures-util", 163 | ] 164 | 165 | [[package]] 166 | name = "futures-channel" 167 | version = "0.3.28" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" 170 | dependencies = [ 171 | "futures-core", 172 | "futures-sink", 173 | ] 174 | 175 | [[package]] 176 | name = "futures-core" 177 | version = "0.3.28" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" 180 | 181 | [[package]] 182 | name = "futures-executor" 183 | version = "0.3.28" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" 186 | dependencies = [ 187 | "futures-core", 188 | "futures-task", 189 | "futures-util", 190 | ] 191 | 192 | [[package]] 193 | name = "futures-io" 194 | version = "0.3.28" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" 197 | 198 | [[package]] 199 | name = "futures-macro" 200 | version = "0.3.28" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" 203 | dependencies = [ 204 | "proc-macro2", 205 | "quote", 206 | "syn 2.0.18", 207 | ] 208 | 209 | [[package]] 210 | name = "futures-sink" 211 | version = "0.3.28" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" 214 | 215 | [[package]] 216 | name = "futures-task" 217 | version = "0.3.28" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" 220 | 221 | [[package]] 222 | name = "futures-util" 223 | version = "0.3.28" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" 226 | dependencies = [ 227 | "futures-channel", 228 | "futures-core", 229 | "futures-io", 230 | "futures-macro", 231 | "futures-sink", 232 | "futures-task", 233 | "memchr", 234 | "pin-project-lite", 235 | "pin-utils", 236 | "slab", 237 | ] 238 | 239 | [[package]] 240 | name = "hermit-abi" 241 | version = "0.2.6" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 244 | dependencies = [ 245 | "libc", 246 | ] 247 | 248 | [[package]] 249 | name = "hermit-abi" 250 | version = "0.3.1" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 253 | 254 | [[package]] 255 | name = "humantime" 256 | version = "2.1.0" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 259 | 260 | [[package]] 261 | name = "inotify" 262 | version = "0.9.6" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" 265 | dependencies = [ 266 | "bitflags", 267 | "inotify-sys", 268 | "libc", 269 | ] 270 | 271 | [[package]] 272 | name = "inotify-sys" 273 | version = "0.1.5" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" 276 | dependencies = [ 277 | "libc", 278 | ] 279 | 280 | [[package]] 281 | name = "io-lifetimes" 282 | version = "1.0.11" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" 285 | dependencies = [ 286 | "hermit-abi 0.3.1", 287 | "libc", 288 | "windows-sys 0.48.0", 289 | ] 290 | 291 | [[package]] 292 | name = "is-terminal" 293 | version = "0.4.7" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" 296 | dependencies = [ 297 | "hermit-abi 0.3.1", 298 | "io-lifetimes", 299 | "rustix", 300 | "windows-sys 0.48.0", 301 | ] 302 | 303 | [[package]] 304 | name = "kqueue" 305 | version = "1.0.7" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" 308 | dependencies = [ 309 | "kqueue-sys", 310 | "libc", 311 | ] 312 | 313 | [[package]] 314 | name = "kqueue-sys" 315 | version = "1.0.3" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" 318 | dependencies = [ 319 | "bitflags", 320 | "libc", 321 | ] 322 | 323 | [[package]] 324 | name = "libc" 325 | version = "0.2.146" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" 328 | 329 | [[package]] 330 | name = "linux-raw-sys" 331 | version = "0.3.8" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" 334 | 335 | [[package]] 336 | name = "lock_api" 337 | version = "0.4.10" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" 340 | dependencies = [ 341 | "autocfg", 342 | "scopeguard", 343 | ] 344 | 345 | [[package]] 346 | name = "log" 347 | version = "0.4.19" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" 350 | 351 | [[package]] 352 | name = "memchr" 353 | version = "2.5.0" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 356 | 357 | [[package]] 358 | name = "mio" 359 | version = "0.8.8" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" 362 | dependencies = [ 363 | "libc", 364 | "log", 365 | "wasi", 366 | "windows-sys 0.48.0", 367 | ] 368 | 369 | [[package]] 370 | name = "notify" 371 | version = "6.0.1" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "5738a2795d57ea20abec2d6d76c6081186709c0024187cd5977265eda6598b51" 374 | dependencies = [ 375 | "bitflags", 376 | "crossbeam-channel", 377 | "filetime", 378 | "fsevent-sys", 379 | "inotify", 380 | "kqueue", 381 | "libc", 382 | "mio", 383 | "walkdir", 384 | "windows-sys 0.45.0", 385 | ] 386 | 387 | [[package]] 388 | name = "num_cpus" 389 | version = "1.15.0" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" 392 | dependencies = [ 393 | "hermit-abi 0.2.6", 394 | "libc", 395 | ] 396 | 397 | [[package]] 398 | name = "parking_lot" 399 | version = "0.12.1" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 402 | dependencies = [ 403 | "lock_api", 404 | "parking_lot_core", 405 | ] 406 | 407 | [[package]] 408 | name = "parking_lot_core" 409 | version = "0.9.8" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" 412 | dependencies = [ 413 | "cfg-if", 414 | "libc", 415 | "redox_syscall 0.3.5", 416 | "smallvec", 417 | "windows-targets 0.48.0", 418 | ] 419 | 420 | [[package]] 421 | name = "pin-project-lite" 422 | version = "0.2.9" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 425 | 426 | [[package]] 427 | name = "pin-utils" 428 | version = "0.1.0" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 431 | 432 | [[package]] 433 | name = "proc-macro2" 434 | version = "1.0.60" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" 437 | dependencies = [ 438 | "unicode-ident", 439 | ] 440 | 441 | [[package]] 442 | name = "quote" 443 | version = "1.0.28" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" 446 | dependencies = [ 447 | "proc-macro2", 448 | ] 449 | 450 | [[package]] 451 | name = "really-notify" 452 | version = "0.1.0" 453 | dependencies = [ 454 | "async-stream", 455 | "bitmask-enum", 456 | "env_logger", 457 | "futures", 458 | "libc", 459 | "log", 460 | "notify", 461 | "thiserror", 462 | "tokio", 463 | ] 464 | 465 | [[package]] 466 | name = "redox_syscall" 467 | version = "0.2.16" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 470 | dependencies = [ 471 | "bitflags", 472 | ] 473 | 474 | [[package]] 475 | name = "redox_syscall" 476 | version = "0.3.5" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" 479 | dependencies = [ 480 | "bitflags", 481 | ] 482 | 483 | [[package]] 484 | name = "regex" 485 | version = "1.8.4" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" 488 | dependencies = [ 489 | "aho-corasick", 490 | "memchr", 491 | "regex-syntax", 492 | ] 493 | 494 | [[package]] 495 | name = "regex-syntax" 496 | version = "0.7.2" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" 499 | 500 | [[package]] 501 | name = "rustix" 502 | version = "0.37.20" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" 505 | dependencies = [ 506 | "bitflags", 507 | "errno", 508 | "io-lifetimes", 509 | "libc", 510 | "linux-raw-sys", 511 | "windows-sys 0.48.0", 512 | ] 513 | 514 | [[package]] 515 | name = "same-file" 516 | version = "1.0.6" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 519 | dependencies = [ 520 | "winapi-util", 521 | ] 522 | 523 | [[package]] 524 | name = "scopeguard" 525 | version = "1.1.0" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 528 | 529 | [[package]] 530 | name = "signal-hook-registry" 531 | version = "1.4.1" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 534 | dependencies = [ 535 | "libc", 536 | ] 537 | 538 | [[package]] 539 | name = "slab" 540 | version = "0.4.8" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" 543 | dependencies = [ 544 | "autocfg", 545 | ] 546 | 547 | [[package]] 548 | name = "smallvec" 549 | version = "1.10.0" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 552 | 553 | [[package]] 554 | name = "socket2" 555 | version = "0.4.9" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" 558 | dependencies = [ 559 | "libc", 560 | "winapi", 561 | ] 562 | 563 | [[package]] 564 | name = "syn" 565 | version = "1.0.109" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 568 | dependencies = [ 569 | "proc-macro2", 570 | "quote", 571 | "unicode-ident", 572 | ] 573 | 574 | [[package]] 575 | name = "syn" 576 | version = "2.0.18" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" 579 | dependencies = [ 580 | "proc-macro2", 581 | "quote", 582 | "unicode-ident", 583 | ] 584 | 585 | [[package]] 586 | name = "termcolor" 587 | version = "1.2.0" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" 590 | dependencies = [ 591 | "winapi-util", 592 | ] 593 | 594 | [[package]] 595 | name = "thiserror" 596 | version = "1.0.40" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" 599 | dependencies = [ 600 | "thiserror-impl", 601 | ] 602 | 603 | [[package]] 604 | name = "thiserror-impl" 605 | version = "1.0.40" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" 608 | dependencies = [ 609 | "proc-macro2", 610 | "quote", 611 | "syn 2.0.18", 612 | ] 613 | 614 | [[package]] 615 | name = "tokio" 616 | version = "1.28.2" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" 619 | dependencies = [ 620 | "autocfg", 621 | "bytes", 622 | "libc", 623 | "mio", 624 | "num_cpus", 625 | "parking_lot", 626 | "pin-project-lite", 627 | "signal-hook-registry", 628 | "socket2", 629 | "tokio-macros", 630 | "windows-sys 0.48.0", 631 | ] 632 | 633 | [[package]] 634 | name = "tokio-macros" 635 | version = "2.1.0" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" 638 | dependencies = [ 639 | "proc-macro2", 640 | "quote", 641 | "syn 2.0.18", 642 | ] 643 | 644 | [[package]] 645 | name = "unicode-ident" 646 | version = "1.0.9" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" 649 | 650 | [[package]] 651 | name = "walkdir" 652 | version = "2.3.3" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" 655 | dependencies = [ 656 | "same-file", 657 | "winapi-util", 658 | ] 659 | 660 | [[package]] 661 | name = "wasi" 662 | version = "0.11.0+wasi-snapshot-preview1" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 665 | 666 | [[package]] 667 | name = "winapi" 668 | version = "0.3.9" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 671 | dependencies = [ 672 | "winapi-i686-pc-windows-gnu", 673 | "winapi-x86_64-pc-windows-gnu", 674 | ] 675 | 676 | [[package]] 677 | name = "winapi-i686-pc-windows-gnu" 678 | version = "0.4.0" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 681 | 682 | [[package]] 683 | name = "winapi-util" 684 | version = "0.1.5" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 687 | dependencies = [ 688 | "winapi", 689 | ] 690 | 691 | [[package]] 692 | name = "winapi-x86_64-pc-windows-gnu" 693 | version = "0.4.0" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 696 | 697 | [[package]] 698 | name = "windows-sys" 699 | version = "0.45.0" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 702 | dependencies = [ 703 | "windows-targets 0.42.2", 704 | ] 705 | 706 | [[package]] 707 | name = "windows-sys" 708 | version = "0.48.0" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 711 | dependencies = [ 712 | "windows-targets 0.48.0", 713 | ] 714 | 715 | [[package]] 716 | name = "windows-targets" 717 | version = "0.42.2" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 720 | dependencies = [ 721 | "windows_aarch64_gnullvm 0.42.2", 722 | "windows_aarch64_msvc 0.42.2", 723 | "windows_i686_gnu 0.42.2", 724 | "windows_i686_msvc 0.42.2", 725 | "windows_x86_64_gnu 0.42.2", 726 | "windows_x86_64_gnullvm 0.42.2", 727 | "windows_x86_64_msvc 0.42.2", 728 | ] 729 | 730 | [[package]] 731 | name = "windows-targets" 732 | version = "0.48.0" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 735 | dependencies = [ 736 | "windows_aarch64_gnullvm 0.48.0", 737 | "windows_aarch64_msvc 0.48.0", 738 | "windows_i686_gnu 0.48.0", 739 | "windows_i686_msvc 0.48.0", 740 | "windows_x86_64_gnu 0.48.0", 741 | "windows_x86_64_gnullvm 0.48.0", 742 | "windows_x86_64_msvc 0.48.0", 743 | ] 744 | 745 | [[package]] 746 | name = "windows_aarch64_gnullvm" 747 | version = "0.42.2" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 750 | 751 | [[package]] 752 | name = "windows_aarch64_gnullvm" 753 | version = "0.48.0" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 756 | 757 | [[package]] 758 | name = "windows_aarch64_msvc" 759 | version = "0.42.2" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 762 | 763 | [[package]] 764 | name = "windows_aarch64_msvc" 765 | version = "0.48.0" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 768 | 769 | [[package]] 770 | name = "windows_i686_gnu" 771 | version = "0.42.2" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 774 | 775 | [[package]] 776 | name = "windows_i686_gnu" 777 | version = "0.48.0" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 780 | 781 | [[package]] 782 | name = "windows_i686_msvc" 783 | version = "0.42.2" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 786 | 787 | [[package]] 788 | name = "windows_i686_msvc" 789 | version = "0.48.0" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 792 | 793 | [[package]] 794 | name = "windows_x86_64_gnu" 795 | version = "0.42.2" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 798 | 799 | [[package]] 800 | name = "windows_x86_64_gnu" 801 | version = "0.48.0" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 804 | 805 | [[package]] 806 | name = "windows_x86_64_gnullvm" 807 | version = "0.42.2" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 810 | 811 | [[package]] 812 | name = "windows_x86_64_gnullvm" 813 | version = "0.48.0" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 816 | 817 | [[package]] 818 | name = "windows_x86_64_msvc" 819 | version = "0.42.2" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 822 | 823 | [[package]] 824 | name = "windows_x86_64_msvc" 825 | version = "0.48.0" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 828 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "really-notify" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Protryon "] 6 | license = "Apache-2.0" 7 | repository = "https://github.com/Protryon/really-notify" 8 | description = "For when you really, really just want to know that your config changed" 9 | keywords = [ "inotify", "notify", "reload" ] 10 | 11 | [dependencies] 12 | log = "0.4" 13 | tokio = { "version" = "1", features = ["full"] } 14 | thiserror = "1.0" 15 | futures = "0.3" 16 | notify = { version = "6.0", optional = true } 17 | libc = { version = "0.2", optional = true } 18 | bitmask-enum = { version = "2.1.0", optional = true } 19 | async-stream = { version = "0.3.5", optional = true } 20 | 21 | [dev-dependencies] 22 | env_logger = "0.10.0" 23 | 24 | [features] 25 | notify = ["dep:notify"] 26 | inotify = ["libc", "bitmask-enum", "async-stream"] 27 | default = ["inotify"] 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # really-notify 3 | 4 | This crate is for when you really, really just want to know that your config changed. K8s configmap symlink shenanigans? No problem. Multi-level symlink directory & file redirections? Sure. Just tell me my damn config changed. 5 | 6 | ## Inspiration 7 | 8 | This crate is a derivation of some code I've been recycling for a while to deal with hot reloading K8s ConfigMaps, which relink the parent dir (accessed through a symlink). I've made it a bit more robust. This is primarily intended for use on Linux/Unix systems, however I've added a backup fallback to `notify` crate. That won't have the great symlink management the native `inotify` integration has. `notify` crate is unable to be configured to deal with symlinks properly. 9 | 10 | Similarly, no existing inotify crate (I could find at a cursory glance) had proper async support. They all delegated out to a blocking thread at best, similar to how Tokio deals with files. To integrate with the Tokio network stack, I'm treating the `inotify` FD as a UNIX pipe receiver, which makes the correct file `read` syscall, but uses `epoll` through `mio`, and not some blocking stuff. Confirmed with `strace`. 11 | 12 | ## Examples 13 | 14 | See `examples/` subdirectory. -------------------------------------------------------------------------------- /examples/config.yaml: -------------------------------------------------------------------------------- 1 | my_config: test 2 | test: west -------------------------------------------------------------------------------- /examples/example.rs: -------------------------------------------------------------------------------- 1 | use really_notify::FileWatcherConfig; 2 | 3 | #[tokio::main] 4 | async fn main() { 5 | env_logger::Builder::new() 6 | .parse_env(env_logger::Env::default().default_filter_or("info")) 7 | .init(); 8 | // the general idea here by bundling the parser in, is that retry logic is embedded. 9 | // if the config becomes invalid (fails to parse, fails validation, etc), then the old config can be used in the meantime 10 | // while `really-notify` spits out errors to the log for the user. 11 | 12 | // if the file doesn't exist, isn't readable, can't be parsed, etc, then `really-notify` will enter a 1-second loop to reattempt and print errors. 13 | // this helps recover against not having read permissions, which prevents us from watching the file for changes as well. 14 | let mut receiver = FileWatcherConfig::new("./examples/config.yaml", "config") 15 | .with_parser(|data| String::from_utf8(data)) 16 | .start(); 17 | while let Some(config) = receiver.recv().await { 18 | // so, everytime we get here, we have a new valid config to throw in an `ArcSwap`/`tokio::sync::watch`/etc. No further validation needed. 19 | println!("got new config!\n{config}"); 20 | } 21 | // when `receiver` is dropped, all of the `inotify` stuff gets cleaned up. 22 | } 23 | -------------------------------------------------------------------------------- /src/backend/inotify.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{HashMap, HashSet}, 3 | ffi::OsString, 4 | fmt::Display, 5 | path::{Path, PathBuf}, 6 | sync::Arc, 7 | }; 8 | 9 | use futures::{pin_mut, StreamExt}; 10 | use log::{debug, error}; 11 | 12 | use crate::{ 13 | inotify::{normalize, INotify, INotifyMask, WatchHandle}, 14 | FileWatcherError, WatcherContext, 15 | }; 16 | 17 | pub(crate) async fn start_backend( 18 | mut watcher_context: WatcherContext, 19 | ) { 20 | tokio::spawn(async move { 21 | watcher_context.file = normalize(&watcher_context.file); 22 | let watcher_context = Arc::new(watcher_context); 23 | loop { 24 | if let Err(e) = load_config::(watcher_context.clone()).await { 25 | error!( 26 | "{} watch error: {e} @ '{}'", 27 | watcher_context.log_name, 28 | watcher_context.file.display() 29 | ); 30 | tokio::time::sleep(watcher_context.retry_interval).await; 31 | } 32 | } 33 | }); 34 | } 35 | 36 | const MAX_ITER: usize = 16; 37 | 38 | pub(crate) async fn load_config( 39 | context: Arc, 40 | ) -> Result<(), FileWatcherError> { 41 | let mut notify = INotify::new()?; 42 | let mut watch_handles = vec![]; 43 | let mut interesting_children: HashMap = HashMap::new(); 44 | let mut symlinks: HashSet = HashSet::new(); 45 | let mut current_main_file = context.file.clone(); 46 | let mut hanging_dirs = vec![]; 47 | let mut seen_dirs: HashSet = HashSet::new(); 48 | loop { 49 | debug!( 50 | "watching main target or link {}", 51 | current_main_file.display() 52 | ); 53 | let main_notify = notify.add_watch( 54 | ¤t_main_file, 55 | INotifyMask::CloseWrite 56 | | INotifyMask::DeleteSelf 57 | | INotifyMask::Modify 58 | | INotifyMask::MoveSelf 59 | | INotifyMask::DontFollow, 60 | )?; 61 | watch_handles.push(main_notify); 62 | if let Some(parent) = current_main_file.parent() { 63 | hanging_dirs.push(( 64 | parent.to_path_buf(), 65 | Some(current_main_file.file_name().unwrap().to_os_string()), 66 | )); 67 | } 68 | let main_file_metadata = tokio::fs::symlink_metadata(¤t_main_file).await?; 69 | if main_file_metadata.is_symlink() { 70 | symlinks.insert(main_notify); 71 | let link = tokio::fs::read_link(¤t_main_file).await?; 72 | current_main_file = if link.is_relative() { 73 | current_main_file.parent().unwrap().join(link) 74 | } else { 75 | link 76 | }; 77 | current_main_file = normalize(¤t_main_file); 78 | } else { 79 | break; 80 | } 81 | } 82 | let mut next_round = hanging_dirs; 83 | let mut round_count = 0usize; 84 | loop { 85 | let mut round = std::mem::take(&mut next_round); 86 | if round.is_empty() || round_count > MAX_ITER { 87 | break; 88 | } 89 | round_count += 1; 90 | while let Some((dir, child)) = round.pop() { 91 | if seen_dirs.contains(&dir) { 92 | continue; 93 | } 94 | let dir_metadata = tokio::fs::symlink_metadata(&dir).await?; 95 | debug!("watching ancestor {}", dir.display()); 96 | if dir_metadata.is_symlink() { 97 | let mut link = tokio::fs::read_link(&dir).await?; 98 | if link.is_relative() { 99 | link = dir.parent().unwrap().join(link); 100 | } 101 | link = normalize(&link); 102 | next_round.push((link, child)); 103 | watch_handles.push(notify.add_watch( 104 | &dir, 105 | INotifyMask::CloseWrite 106 | | INotifyMask::DeleteSelf 107 | | INotifyMask::Modify 108 | | INotifyMask::MoveSelf 109 | | INotifyMask::DontFollow, 110 | )?); 111 | } else { 112 | let watcher = notify.add_watch( 113 | &dir, 114 | INotifyMask::Delete 115 | | INotifyMask::DeleteSelf 116 | | INotifyMask::Modify 117 | | INotifyMask::MoveSelf 118 | | INotifyMask::MovedFrom 119 | | INotifyMask::MovedTo 120 | | INotifyMask::DontFollow, 121 | )?; 122 | watch_handles.push(watcher); 123 | interesting_children 124 | .insert(watcher, child.expect("missing child for non-symlink root")); 125 | } 126 | seen_dirs.insert(dir.clone()); 127 | let mut current_child: &Path = &dir; 128 | let mut current_parent = current_child.parent(); 129 | while let Some(parent) = current_parent { 130 | if seen_dirs.contains(parent) { 131 | break; 132 | } 133 | debug!("watching ancestor {}", parent.display()); 134 | let metadata = tokio::fs::symlink_metadata(parent).await?; 135 | if metadata.is_symlink() { 136 | let mut link = tokio::fs::read_link(parent).await?; 137 | if link.is_relative() { 138 | link = dir.parent().unwrap().join(link); 139 | } 140 | link = normalize(&link); 141 | next_round.push(( 142 | link, 143 | Some(current_child.file_name().unwrap().to_os_string()), 144 | )); 145 | watch_handles.push(notify.add_watch( 146 | parent, 147 | INotifyMask::CloseWrite 148 | | INotifyMask::DeleteSelf 149 | | INotifyMask::Modify 150 | | INotifyMask::MoveSelf 151 | | INotifyMask::DontFollow, 152 | )?); 153 | } else { 154 | let watcher = notify.add_watch( 155 | parent, 156 | INotifyMask::Delete 157 | | INotifyMask::DeleteSelf 158 | | INotifyMask::Modify 159 | | INotifyMask::MoveSelf 160 | | INotifyMask::MovedFrom 161 | | INotifyMask::MovedTo 162 | | INotifyMask::DontFollow, 163 | )?; 164 | watch_handles.push(watcher); 165 | interesting_children 166 | .insert(watcher, current_child.file_name().unwrap().to_os_string()); 167 | } 168 | seen_dirs.insert(parent.to_path_buf()); 169 | 170 | current_child = parent; 171 | current_parent = parent.parent(); 172 | } 173 | } 174 | } 175 | 176 | let stream = notify.stream(); 177 | pin_mut!(stream); 178 | while let Some(event) = stream.next().await { 179 | let event = match event { 180 | Err(e) => { 181 | return Err(e.into()); 182 | } 183 | Ok(x) => x, 184 | }; 185 | debug!("received event {event:?}"); 186 | if let Some(interest) = interesting_children.get(&event.watch_descriptor) { 187 | // a directory event we need to filter, and if applicable, always full refresh 188 | if &event.name != interest { 189 | continue; 190 | } 191 | context.notify.notify_one(); 192 | 193 | return Ok(()); 194 | } else if symlinks.contains(&event.watch_descriptor) { 195 | // a symlink changed, we always reload and need a full refresh 196 | context.notify.notify_one(); 197 | return Ok(()); 198 | } else { 199 | // the underlying file was modified, we don't need to full refresh 200 | context.notify.notify_one(); 201 | } 202 | } 203 | Ok(()) 204 | } 205 | -------------------------------------------------------------------------------- /src/backend/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all( 2 | feature = "notify", 3 | not(all(feature = "inotify", target_family = "unix")) 4 | ))] 5 | mod notify; 6 | #[cfg(all( 7 | feature = "notify", 8 | not(all(feature = "inotify", target_family = "unix")) 9 | ))] 10 | pub(crate) use self::notify::*; 11 | 12 | #[cfg(all(feature = "inotify", target_family = "unix"))] 13 | mod inotify; 14 | #[cfg(all(feature = "inotify", target_family = "unix"))] 15 | pub(crate) use inotify::*; 16 | -------------------------------------------------------------------------------- /src/backend/notify.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, sync::Arc}; 2 | 3 | use log::{debug, error, info}; 4 | use notify::{ 5 | event::{AccessKind, AccessMode}, 6 | EventKind, RecursiveMode, Watcher, 7 | }; 8 | use tokio::sync::oneshot; 9 | 10 | use crate::{FileWatcherError, WatcherContext}; 11 | 12 | pub(crate) async fn start_backend(watcher_context: WatcherContext) { 13 | tokio::task::spawn_blocking(move || { 14 | let watcher_context = Arc::new(watcher_context); 15 | loop { 16 | match load_config::(watcher_context.clone()) { 17 | Ok(()) => break, 18 | Err(e) => { 19 | error!( 20 | "failed to setup {} watcher: {e} @ '{}', retrying in {:.1} second(s)", 21 | watcher_context.log_name, 22 | watcher_context.file.display(), 23 | watcher_context.retry_interval.as_secs_f64() 24 | ); 25 | std::thread::sleep(watcher_context.retry_interval); 26 | } 27 | } 28 | } 29 | }) 30 | .await 31 | .unwrap(); 32 | } 33 | 34 | fn load_config( 35 | context: Arc, 36 | ) -> Result<(), FileWatcherError> { 37 | let (watcher_sender, watcher_receiver) = oneshot::channel(); 38 | let mut watcher_receiver = Some(watcher_receiver); 39 | 40 | let context2 = context.clone(); 41 | let realpath = std::fs::canonicalize(&context2.file)?; 42 | let realpath2 = realpath.clone(); 43 | 44 | let mut watcher = notify::recommended_watcher( 45 | move |res: Result| { 46 | if watcher_receiver.is_none() { 47 | return; 48 | } 49 | match res { 50 | Ok(event) => { 51 | match event.kind { 52 | EventKind::Access(AccessKind::Close(AccessMode::Write)) 53 | | EventKind::Modify(_) 54 | | EventKind::Remove(_) => (), 55 | _ => return, 56 | } 57 | let mut found_path = false; 58 | for path in &event.paths { 59 | if context 60 | .file 61 | .ancestors() 62 | .chain(realpath2.ancestors()) 63 | .any(|x| x == path) 64 | { 65 | found_path = true; 66 | break; 67 | } 68 | } 69 | if !found_path { 70 | return; 71 | } 72 | debug!("file updated: {:?}", event.paths); 73 | context.notify.notify_one(); 74 | watcher_receiver.take().unwrap().blocking_recv().ok(); 75 | while let Err(e) = load_config::(context.clone()) { 76 | error!("failed to reload {} watcher: {e} @ '{}', retrying in {:.1} second(s)...", context.log_name, context.file.display(), context.retry_interval.as_secs_f64()); 77 | std::thread::sleep(context.retry_interval); 78 | context.notify.notify_one(); 79 | } 80 | } 81 | Err(e) => { 82 | error!( 83 | "{} watch error: {e} @ '{}'", 84 | context.log_name, 85 | context.file.display() 86 | ); 87 | } 88 | } 89 | }, 90 | )?; 91 | for ancestor in context2.file.ancestors() { 92 | debug!("watching {}", ancestor.display()); 93 | watcher.watch(&ancestor, RecursiveMode::NonRecursive)?; 94 | } 95 | for ancestor in realpath.ancestors() { 96 | debug!("watching {}", ancestor.display()); 97 | watcher.watch(&ancestor, RecursiveMode::NonRecursive)?; 98 | } 99 | watcher_sender.send(watcher).ok(); 100 | 101 | Ok(()) 102 | } 103 | -------------------------------------------------------------------------------- /src/inotify.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::{CString, OsString}, 3 | fs::File, 4 | io::Error as IoError, 5 | os::{ 6 | fd::{AsRawFd, FromRawFd}, 7 | unix::prelude::{OsStrExt, OsStringExt}, 8 | }, 9 | path::{Component, Path, PathBuf}, 10 | }; 11 | 12 | use async_stream::stream; 13 | use bitmask_enum::bitmask; 14 | use futures::Stream; 15 | use log::debug; 16 | use tokio::{io::AsyncReadExt, net::unix::pipe::Receiver}; 17 | 18 | pub struct INotify { 19 | stream: Receiver, 20 | } 21 | 22 | #[bitmask(u32)] 23 | pub enum INotifyMask { 24 | Access = libc::IN_ACCESS, 25 | AttributeChanged = libc::IN_ATTRIB, 26 | CloseWrite = libc::IN_CLOSE_WRITE, 27 | CloseNoWrite = libc::IN_CLOSE_NOWRITE, 28 | Create = libc::IN_CREATE, 29 | Delete = libc::IN_DELETE, 30 | DeleteSelf = libc::IN_DELETE_SELF, 31 | Modify = libc::IN_MODIFY, 32 | MoveSelf = libc::IN_MOVE_SELF, 33 | MovedFrom = libc::IN_MOVED_FROM, 34 | MovedTo = libc::IN_MOVED_TO, 35 | Open = libc::IN_OPEN, 36 | // meta mask flags for create only 37 | DontFollow = libc::IN_DONT_FOLLOW, 38 | ExclUnlink = libc::IN_EXCL_UNLINK, 39 | MaskAdd = libc::IN_MASK_ADD, 40 | Oneshot = libc::IN_ONESHOT, 41 | OnlyDir = libc::IN_ONLYDIR, 42 | MaskCreate = libc::IN_MASK_CREATE, 43 | // meta mask flags only returned in read 44 | Ignored = libc::IN_IGNORED, 45 | IsDir = libc::IN_ISDIR, 46 | QueueOverflow = libc::IN_Q_OVERFLOW, 47 | Unmount = libc::IN_UNMOUNT, 48 | } 49 | 50 | #[repr(C)] 51 | #[derive(Copy, Clone)] 52 | struct RawINotifyEvent { 53 | watch_descriptor: WatchHandle, 54 | mask: INotifyMask, 55 | cookie: u32, 56 | len: u32, 57 | // inline CString 58 | } 59 | 60 | const EVENT_SIZE: usize = std::mem::size_of::(); 61 | 62 | #[derive(Debug)] 63 | pub struct INotifyEvent { 64 | pub watch_descriptor: WatchHandle, 65 | pub mask: INotifyMask, 66 | pub cookie: u32, 67 | pub name: OsString, 68 | } 69 | 70 | #[derive(PartialEq, Eq, Debug, Clone, Copy, PartialOrd, Ord, Hash)] 71 | #[repr(transparent)] 72 | pub struct WatchHandle(i32); 73 | 74 | pub(crate) fn normalize(path: &Path) -> PathBuf { 75 | if !path.is_absolute() { 76 | panic!("attempted to normalize a relative path"); 77 | } 78 | let mut out = PathBuf::new(); 79 | for component in path.components() { 80 | match component { 81 | Component::Prefix(_) => unreachable!(), 82 | Component::RootDir => out.push("/"), 83 | Component::CurDir => out.push("."), 84 | Component::ParentDir => out.push(".."), 85 | Component::Normal(component) => out.push(component), 86 | } 87 | } 88 | out 89 | } 90 | 91 | const NAME_MAX: usize = 255; 92 | 93 | impl INotify { 94 | pub fn new() -> Result { 95 | let fd = unsafe { libc::inotify_init1(libc::IN_NONBLOCK | libc::IN_CLOEXEC) }; 96 | if fd < 0 { 97 | return Err(IoError::last_os_error()); 98 | } 99 | let file = unsafe { File::from_raw_fd(fd) }; 100 | let stream = Receiver::from_file_unchecked(file)?; 101 | Ok(Self { stream }) 102 | } 103 | 104 | pub fn add_watch( 105 | &self, 106 | path: impl AsRef, 107 | mask: INotifyMask, 108 | ) -> Result { 109 | let pathd = path.as_ref(); 110 | let path = CString::new(path.as_ref().as_os_str().as_bytes()).expect("NUL byte in path"); 111 | let descriptor = 112 | unsafe { libc::inotify_add_watch(self.stream.as_raw_fd(), path.as_ptr(), mask.bits()) }; 113 | if descriptor < 0 { 114 | return Err(IoError::last_os_error()); 115 | } 116 | debug!("watching {descriptor}: {} {mask:?}", pathd.display()); 117 | Ok(WatchHandle(descriptor)) 118 | } 119 | 120 | #[allow(dead_code)] 121 | pub fn rm_watch(&self, handle: WatchHandle) -> Result<(), IoError> { 122 | let out = unsafe { libc::inotify_rm_watch(self.stream.as_raw_fd(), handle.0) }; 123 | if out < 0 { 124 | return Err(IoError::last_os_error()); 125 | } 126 | Ok(()) 127 | } 128 | 129 | pub fn stream<'a>(&'a mut self) -> impl Stream> + 'a { 130 | stream! { 131 | let mut buf = [0u8; EVENT_SIZE + NAME_MAX + 1]; 132 | loop { 133 | let read_len = self.stream.read(&mut buf[..EVENT_SIZE + NAME_MAX + 1]).await?; 134 | let mut buf_ref = &mut buf[..read_len]; 135 | while !buf_ref.is_empty() { 136 | if buf_ref.len() < EVENT_SIZE { 137 | continue; 138 | } 139 | let raw_event: RawINotifyEvent = *unsafe { (buf_ref.as_ptr() as *const RawINotifyEvent).as_ref().unwrap() }; 140 | buf_ref = &mut buf_ref[EVENT_SIZE..]; 141 | if read_len - EVENT_SIZE < raw_event.len as usize { 142 | continue; 143 | } 144 | let mut path = &buf_ref[..raw_event.len as usize]; 145 | while !path.is_empty() && path[path.len() - 1] == 0 { 146 | path = &path[..path.len() - 1]; 147 | } 148 | let path = OsString::from_vec(path.to_vec()); 149 | buf_ref = &mut buf_ref[raw_event.len as usize..]; 150 | yield Ok(INotifyEvent { 151 | watch_descriptor: raw_event.watch_descriptor, 152 | mask: raw_event.mask, 153 | cookie: raw_event.cookie, 154 | name: path, 155 | }); 156 | } 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::{self, Display}, 3 | path::{Path, PathBuf}, 4 | sync::Arc, 5 | time::Duration, 6 | }; 7 | 8 | use backend::start_backend; 9 | use log::{error, info}; 10 | use thiserror::Error; 11 | use tokio::{ 12 | select, 13 | sync::{mpsc, Notify}, 14 | }; 15 | 16 | mod backend; 17 | #[cfg(all(feature = "inotify", target_family = "unix"))] 18 | mod inotify; 19 | 20 | /// `really-notify` primary input. 21 | /// [`T`] is the target parse type, i.e. your serde-deserializable `Config` struct. 22 | /// [`E`] is the generic error type that your parser can fail with. 23 | pub struct FileWatcherConfig { 24 | /// Cosmetic, used for logs to be consistent with application terminology 25 | pub log_name: String, 26 | /// Path to the file you are interested in changes of. Do your worse with symlinks here. 27 | pub file: PathBuf, 28 | /// Parser function to transform a modified target file into our desired output. If you just want raw bytes, you can pass it through, or not set this at all. 29 | pub parser: Arc) -> Result + Send + Sync>, 30 | /// Defaults to one second, how often to attempt reparsing/error recovery. 31 | pub retry_interval: Duration, 32 | } 33 | 34 | #[derive(Error, Debug)] 35 | enum FileWatcherError { 36 | #[error("{0}")] 37 | Io(#[from] std::io::Error), 38 | #[cfg(feature = "notify")] 39 | #[error("{0}")] 40 | Notify(#[from] notify::Error), 41 | #[error("{0}")] 42 | Parse(E), 43 | } 44 | 45 | pub(crate) struct WatcherContext { 46 | pub(crate) file: PathBuf, 47 | pub(crate) log_name: String, 48 | pub(crate) retry_interval: Duration, 49 | pub(crate) notify: Arc, 50 | } 51 | 52 | /// Impossible to fail converting a Vec to a Vec 53 | pub enum Infallible {} 54 | 55 | impl fmt::Display for Infallible { 56 | fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result { 57 | unreachable!() 58 | } 59 | } 60 | 61 | impl FileWatcherConfig, Infallible> { 62 | pub fn new(file: impl AsRef, log_name: impl AsRef) -> Self { 63 | Self { 64 | file: file.as_ref().to_path_buf(), 65 | log_name: log_name.as_ref().to_string(), 66 | parser: Arc::new(|x| Ok(x)), 67 | retry_interval: Duration::from_secs(1), 68 | } 69 | } 70 | } 71 | 72 | impl FileWatcherConfig { 73 | /// Set a new parser and adjust the FileWatcherConfig type parameters as needed. 74 | pub fn with_parser( 75 | self, 76 | func: impl Fn(Vec) -> Result + Send + Sync + 'static, 77 | ) -> FileWatcherConfig { 78 | FileWatcherConfig { 79 | log_name: self.log_name, 80 | file: self.file, 81 | parser: Arc::new(func), 82 | retry_interval: self.retry_interval, 83 | } 84 | } 85 | 86 | /// Set an alternative retry_interval 87 | pub fn with_retry_interval(mut self, retry_interval: Duration) -> Self { 88 | self.retry_interval = retry_interval; 89 | self 90 | } 91 | 92 | /// Run the watcher. Dropping/closing this receiver will cause an immediate cleanup. 93 | pub fn start(self) -> mpsc::Receiver { 94 | let (sender, receiver) = mpsc::channel(3); 95 | tokio::spawn(self.run(sender)); 96 | receiver 97 | } 98 | 99 | async fn run(self, sender: mpsc::Sender) { 100 | let target = loop { 101 | match self.read_target().await { 102 | Ok(x) => break x, 103 | Err(e) => { 104 | error!( 105 | "failed to read initial {}: {e} @ '{}', retrying in {:.1} second(s)", 106 | self.log_name, 107 | self.file.display(), 108 | self.retry_interval.as_secs_f64(), 109 | ); 110 | tokio::time::sleep(self.retry_interval).await; 111 | } 112 | } 113 | }; 114 | if sender.send(target).await.is_err() { 115 | return; 116 | } 117 | let mut file = self.file.clone(); 118 | if file.is_relative() { 119 | if let Ok(cwd) = std::env::current_dir() { 120 | file = cwd.join(file); 121 | } 122 | } 123 | let notify = Arc::new(Notify::new()); 124 | let watcher_context = WatcherContext { 125 | file, 126 | log_name: self.log_name.clone(), 127 | retry_interval: self.retry_interval, 128 | notify: notify.clone(), 129 | }; 130 | start_backend::(watcher_context).await; 131 | loop { 132 | select! { 133 | _ = notify.notified() => { 134 | let target = loop { 135 | match self.read_target().await { 136 | Ok(x) => break x, 137 | Err(e) => { 138 | error!("failed to read {} update: {e} @ {}, retrying in {:.1} second(s)", self.log_name, self.file.display(), self.retry_interval.as_secs_f64()); 139 | tokio::time::sleep(self.retry_interval).await; 140 | // toss out any pending notification, since we will already try again 141 | let notify = notify.notified(); 142 | futures::pin_mut!(notify); 143 | notify.enable(); 144 | } 145 | } 146 | }; 147 | if sender.send(target).await.is_err() { 148 | return; 149 | } 150 | }, 151 | _ = sender.closed() => { 152 | return; 153 | } 154 | } 155 | } 156 | } 157 | 158 | async fn read_target(&self) -> Result> { 159 | info!( 160 | "reading updated {} '{}'", 161 | self.log_name, 162 | self.file.display() 163 | ); 164 | let raw = tokio::fs::read(&self.file).await?; 165 | (self.parser)(raw).map_err(FileWatcherError::Parse) 166 | } 167 | } 168 | 169 | #[cfg(test)] 170 | mod tests { 171 | use super::*; 172 | 173 | #[tokio::test] 174 | async fn test_file_zone() { 175 | env_logger::Builder::new() 176 | .parse_env(env_logger::Env::default().default_filter_or("info")) 177 | .init(); 178 | let mut receiver = FileWatcherConfig::new("./test.yaml", "config").start(); 179 | while let Some(_update) = receiver.recv().await { 180 | println!("updated!"); 181 | } 182 | } 183 | } 184 | --------------------------------------------------------------------------------