Start on watchexec v2
This commit is contained in:
parent
e21a3a99f6
commit
b15615bbaa
|
@ -8,9 +8,8 @@ charset = utf-8
|
|||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[Makefile]
|
||||
[lib/src/*.rs]
|
||||
indent_style = tab
|
||||
indent_size = 8
|
||||
|
||||
[*.toml]
|
||||
indent_size = 2
|
||||
|
|
|
@ -144,6 +144,12 @@ version = "1.4.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
|
||||
|
||||
[[package]]
|
||||
name = "cache-padded"
|
||||
version = "1.1.1"
|
||||
|
@ -195,11 +201,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clearscreen"
|
||||
version = "1.0.4"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9eff80751e49709362458c0b612c3c682693d7a58966e0bf429872888f647e20"
|
||||
checksum = "5a657a584d3350fd861a098df2c84e39bba869546101e4097fe123aca98c2d8d"
|
||||
dependencies = [
|
||||
"nix 0.20.0",
|
||||
"nix 0.22.0",
|
||||
"terminfo",
|
||||
"thiserror",
|
||||
"which",
|
||||
|
@ -235,9 +241,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "command-group"
|
||||
version = "1.0.3"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba0e6179c8bfe0251e18e65da6f251f81451ead1968aa3c24401b425fc1a184b"
|
||||
checksum = "758ddf93da6b6b45c6e44a73d07362945b98814e8c9bb57a8c9353f921c18ba3"
|
||||
dependencies = [
|
||||
"nix 0.22.0",
|
||||
"winapi 0.3.9",
|
||||
|
@ -271,6 +277,16 @@ version = "0.1.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.5"
|
||||
|
@ -505,7 +521,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
|
||||
dependencies = [
|
||||
"bitflags 1.2.1",
|
||||
"fsevent-sys",
|
||||
"fsevent-sys 2.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -517,6 +533,15 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c0e564d24da983c053beff1bb7178e237501206840a3e6bf4e267b9e8ae734a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-zircon"
|
||||
version = "0.3.3"
|
||||
|
@ -689,6 +714,15 @@ dependencies = [
|
|||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
|
@ -712,6 +746,17 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b031475cb1b103ee221afb806a23d35e0570bf7271d7588762ceba8127ed43b3"
|
||||
dependencies = [
|
||||
"bitflags 1.2.1",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
|
@ -779,6 +824,26 @@ dependencies = [
|
|||
"winapi-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "058a107a784f8be94c7d35c1300f4facced2e93d2fbe5b1452b44e905ddca4a9"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587"
|
||||
dependencies = [
|
||||
"bitflags 1.2.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
|
@ -803,6 +868,15 @@ version = "0.5.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.14"
|
||||
|
@ -848,6 +922,28 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miette"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5022841ae4424a04441131a02ea2cc5eafeb1b2cf337b1b3281f57c809b8eb32"
|
||||
dependencies = [
|
||||
"indenter",
|
||||
"miette-derive",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miette-derive"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b33d4f2f59c7ad2219e01e5a20abc6bab961a4275c3692e99abb17d04c83278"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote 1.0.9",
|
||||
"syn 1.0.73",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.4.4"
|
||||
|
@ -871,12 +967,25 @@ dependencies = [
|
|||
"kernel32-sys",
|
||||
"libc",
|
||||
"log",
|
||||
"miow",
|
||||
"miow 0.2.2",
|
||||
"net2",
|
||||
"slab",
|
||||
"winapi 0.2.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.7.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"miow 0.3.7",
|
||||
"ntapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio-extras"
|
||||
version = "2.0.6"
|
||||
|
@ -885,7 +994,7 @@ checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
|
|||
dependencies = [
|
||||
"lazycell",
|
||||
"log",
|
||||
"mio",
|
||||
"mio 0.6.23",
|
||||
"slab",
|
||||
]
|
||||
|
||||
|
@ -901,6 +1010,15 @@ dependencies = [
|
|||
"ws2_32-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miow"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
|
||||
dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nb-connect"
|
||||
version = "1.2.0"
|
||||
|
@ -935,18 +1053,6 @@ dependencies = [
|
|||
"void",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a"
|
||||
dependencies = [
|
||||
"bitflags 1.2.1",
|
||||
"cc",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.22.0"
|
||||
|
@ -979,15 +1085,33 @@ dependencies = [
|
|||
"bitflags 1.2.1",
|
||||
"filetime",
|
||||
"fsevent",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"fsevent-sys 2.0.1",
|
||||
"inotify 0.7.1",
|
||||
"libc",
|
||||
"mio",
|
||||
"mio 0.6.23",
|
||||
"mio-extras",
|
||||
"walkdir",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "5.0.0-pre.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c614e7ed2b1cf82ec99aeffd8cf6225ef5021b9951148eb161393c394855032c"
|
||||
dependencies = [
|
||||
"bitflags 1.2.1",
|
||||
"crossbeam-channel",
|
||||
"filetime",
|
||||
"fsevent-sys 4.0.0",
|
||||
"inotify 0.9.3",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"mio 0.7.13",
|
||||
"walkdir",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-rust"
|
||||
version = "4.5.2"
|
||||
|
@ -1002,6 +1126,15 @@ dependencies = [
|
|||
"zvariant_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
|
||||
dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.44"
|
||||
|
@ -1021,6 +1154,16 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc"
|
||||
version = "0.2.7"
|
||||
|
@ -1077,6 +1220,31 @@ version = "2.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
|
||||
dependencies = [
|
||||
"instant",
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"instant",
|
||||
"libc",
|
||||
"redox_syscall 0.2.9",
|
||||
"smallvec",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.8.0"
|
||||
|
@ -1377,6 +1545,12 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.126"
|
||||
|
@ -1440,6 +1614,15 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
version = "1.3.0"
|
||||
|
@ -1448,9 +1631,9 @@ checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec"
|
|||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbce6d4507c7e4a3962091436e56e95290cb71fa302d0d270e32130b75fbff27"
|
||||
checksum = "729a25c17d72b06c68cb47955d44fda88ad2d3e7d77e025663fdd69b93dd71a1"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
|
@ -1458,6 +1641,12 @@ version = "0.4.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.4.1"
|
||||
|
@ -1609,6 +1798,37 @@ dependencies = [
|
|||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
"libc",
|
||||
"memchr",
|
||||
"mio 0.7.13",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"tokio-macros",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote 1.0.9",
|
||||
"syn 1.0.73",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.8"
|
||||
|
@ -1774,6 +1994,20 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
|||
[[package]]
|
||||
name = "watchexec"
|
||||
version = "1.17.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"miette",
|
||||
"notify 5.0.0-pre.11",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "watchexec"
|
||||
version = "1.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c52e0868bc57765fa91593a173323f464855e53b27f779e1110ba0fb4cb6b406"
|
||||
dependencies = [
|
||||
"clearscreen",
|
||||
"command-group",
|
||||
|
@ -1783,7 +2017,7 @@ dependencies = [
|
|||
"lazy_static",
|
||||
"log",
|
||||
"nix 0.22.0",
|
||||
"notify",
|
||||
"notify 4.0.17",
|
||||
"walkdir",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
@ -1800,7 +2034,7 @@ dependencies = [
|
|||
"insta",
|
||||
"log",
|
||||
"notify-rust",
|
||||
"watchexec",
|
||||
"watchexec 1.17.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1814,11 +2048,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "4.1.0"
|
||||
version = "4.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b55551e42cbdf2ce2bedd2203d0cc08dba002c27510f86dab6d0ce304cba3dfe"
|
||||
checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9"
|
||||
dependencies = [
|
||||
"either",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
]
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
hard_tabs = true
|
|
@ -15,18 +15,13 @@ readme = "README.md"
|
|||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
clearscreen = "1.0.4"
|
||||
command-group = "1.0.3"
|
||||
derive_builder = "0.10.0"
|
||||
glob = "0.3.0"
|
||||
globset = "=0.4.6"
|
||||
lazy_static = "1.1.0"
|
||||
log = "0.4.14"
|
||||
notify = "4.0.15"
|
||||
walkdir = "2.3.2"
|
||||
chrono = "0.4.19"
|
||||
miette = "0.7.0"
|
||||
notify = "5.0.0-pre.11"
|
||||
thiserror = "1.0.26"
|
||||
tracing = "0.1.26"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = "0.22.0"
|
||||
[dependencies.tokio]
|
||||
version = "1.10.0"
|
||||
features = ["full"]
|
||||
|
||||
[target.'cfg(windows)'.dependencies.winapi]
|
||||
version = "0.3.9"
|
||||
|
|
|
@ -1,151 +0,0 @@
|
|||
//! Configuration for watchexec.
|
||||
//!
|
||||
//! The [`Config`] struct is not constructable, use [`ConfigBuilder`].
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```
|
||||
//! # use watchexec::config::ConfigBuilder;
|
||||
//! ConfigBuilder::default()
|
||||
//! .cmd(vec!["echo hello world".into()])
|
||||
//! .paths(vec![".".into()])
|
||||
//! .build()
|
||||
//! .expect("mission failed");
|
||||
//! ```
|
||||
|
||||
use derive_builder::Builder;
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
|
||||
use crate::run::OnBusyUpdate;
|
||||
use crate::Shell;
|
||||
|
||||
/// Arguments to the watcher
|
||||
#[derive(Builder, Clone, Debug)]
|
||||
#[builder(setter(into, strip_option))]
|
||||
#[builder(build_fn(validate = "Self::validate"))]
|
||||
#[non_exhaustive]
|
||||
pub struct Config {
|
||||
/// Command to execute.
|
||||
///
|
||||
/// When `shell` is [`Shell::None`], this is expected to be in “execvp(3)”
|
||||
/// format: first program, rest arguments. Otherwise, all elements will be
|
||||
/// joined together with a single space and passed to the shell. More
|
||||
/// control can then be obtained by providing a 1-element vec, and doing
|
||||
/// your own joining and/or escaping there.
|
||||
pub cmd: Vec<String>,
|
||||
|
||||
/// List of paths to watch for changes.
|
||||
pub paths: Vec<PathBuf>,
|
||||
|
||||
/// Positive filters (trigger only on matching changes). Glob format.
|
||||
#[builder(default)]
|
||||
pub filters: Vec<String>,
|
||||
|
||||
/// Negative filters (do not trigger on matching changes). Glob format.
|
||||
#[builder(default)]
|
||||
pub ignores: Vec<String>,
|
||||
|
||||
/// Clear the screen before each run.
|
||||
#[builder(default)]
|
||||
pub clear_screen: bool,
|
||||
|
||||
/// If Some, send that signal (e.g. SIGHUP) to the command on change.
|
||||
#[builder(default)]
|
||||
pub signal: Option<String>,
|
||||
|
||||
/// Specify what to do when receiving updates while the command is running.
|
||||
#[builder(default)]
|
||||
pub on_busy_update: OnBusyUpdate,
|
||||
|
||||
/// Interval to debounce the changes.
|
||||
#[builder(default = "Duration::from_millis(100)")]
|
||||
pub debounce: Duration,
|
||||
|
||||
/// Run the commands right after starting.
|
||||
#[builder(default = "true")]
|
||||
pub run_initially: bool,
|
||||
|
||||
/// Specify the shell to use.
|
||||
#[builder(default)]
|
||||
pub shell: Shell,
|
||||
|
||||
/// Ignore metadata changes.
|
||||
#[builder(default)]
|
||||
pub no_meta: bool,
|
||||
|
||||
/// Do not set WATCHEXEC_*_PATH environment variables for the process.
|
||||
#[builder(default)]
|
||||
pub no_environment: bool,
|
||||
|
||||
/// Skip auto-loading .gitignore files
|
||||
#[builder(default)]
|
||||
pub no_vcs_ignore: bool,
|
||||
|
||||
/// Skip auto-loading .ignore files
|
||||
#[builder(default)]
|
||||
pub no_ignore: bool,
|
||||
|
||||
/// For testing only, always set to false.
|
||||
#[builder(setter(skip), default)]
|
||||
#[doc(hidden)]
|
||||
pub once: bool,
|
||||
|
||||
/// Force using the polling backend.
|
||||
#[builder(default)]
|
||||
pub poll: bool,
|
||||
|
||||
/// Interval for polling.
|
||||
#[builder(default = "Duration::from_secs(1)")]
|
||||
pub poll_interval: Duration,
|
||||
|
||||
/// Whether to use a process group to run the command.
|
||||
#[builder(default = "true")]
|
||||
pub use_process_group: bool,
|
||||
}
|
||||
|
||||
impl ConfigBuilder {
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
if self.cmd.as_ref().map_or(true, Vec::is_empty) {
|
||||
return Err("cmd must not be empty".into());
|
||||
}
|
||||
|
||||
if self.paths.as_ref().map_or(true, Vec::is_empty) {
|
||||
return Err("paths must not be empty".into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[deprecated(since = "1.15.0", note = "does nothing. set the log level instead")]
|
||||
pub fn debug(&mut self, _: impl Into<bool>) -> &mut Self {
|
||||
self
|
||||
}
|
||||
|
||||
/// Do not wrap the commands in a shell.
|
||||
#[deprecated(since = "1.15.0", note = "use shell(Shell::None) instead")]
|
||||
pub fn no_shell(&mut self, s: impl Into<bool>) -> &mut Self {
|
||||
if s.into() {
|
||||
self.shell(Shell::default())
|
||||
} else {
|
||||
self.shell(Shell::None)
|
||||
}
|
||||
}
|
||||
|
||||
#[deprecated(since = "1.15.0", note = "use on_busy_update(Restart) instead")]
|
||||
pub fn restart(&mut self, b: impl Into<bool>) -> &mut Self {
|
||||
if b.into() {
|
||||
self.on_busy_update(OnBusyUpdate::Restart)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[deprecated(since = "1.15.0", note = "use on_busy_update(DoNothing) instead")]
|
||||
pub fn watch_when_idle(&mut self, b: impl Into<bool>) -> &mut Self {
|
||||
if b.into() {
|
||||
self.on_busy_update(OnBusyUpdate::DoNothing)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
141
lib/src/error.rs
141
lib/src/error.rs
|
@ -1,86 +1,69 @@
|
|||
use std::{error::Error as StdError, fmt, io, sync::PoisonError};
|
||||
//! Watchexec has two error types: for critical and for runtime errors.
|
||||
|
||||
pub type Result<T> = ::std::result::Result<T, Error>;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use thiserror::Error;
|
||||
use miette::Diagnostic;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::{event::Event, fs::Watcher};
|
||||
|
||||
/// Errors which are not recoverable and stop watchexec execution.
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
Canonicalization(String, io::Error),
|
||||
Glob(globset::Error),
|
||||
Io(io::Error),
|
||||
Notify(notify::Error),
|
||||
Generic(String),
|
||||
PoisonedLock,
|
||||
ClearScreen(clearscreen::Error),
|
||||
pub enum CriticalError {
|
||||
/// A critical I/O error occurred.
|
||||
#[error(transparent)]
|
||||
#[diagnostic(code(watchexec::critical::io_error))]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
/// Error received when an event cannot be sent to the errors channel.
|
||||
#[error("cannot send internal runtime error: {0}")]
|
||||
#[diagnostic(code(watchexec::critical::error_channel_send))]
|
||||
ErrorChannelSend(#[from] mpsc::error::SendError<RuntimeError>),
|
||||
}
|
||||
|
||||
impl StdError for Error {}
|
||||
/// Errors which _may_ be recoverable, transient, or only affect a part of the operation, and should
|
||||
/// be reported to the user and/or acted upon programatically, but will not outright stop watchexec.
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum RuntimeError {
|
||||
/// Generic I/O error, with no additional context.
|
||||
#[error(transparent)]
|
||||
#[diagnostic(code(watchexec::runtime::io_error))]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(err: String) -> Self {
|
||||
Self::Generic(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<globset::Error> for Error {
|
||||
fn from(err: globset::Error) -> Self {
|
||||
Self::Glob(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Self {
|
||||
Self::Io(match err.raw_os_error() {
|
||||
Some(7) => io::Error::new(io::ErrorKind::Other, "There are so many changed files that the environment variables of the command have been overrun. Try running with --no-meta or --no-environment."),
|
||||
_ => err,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<notify::Error> for Error {
|
||||
fn from(err: notify::Error) -> Self {
|
||||
match err {
|
||||
notify::Error::Io(err) => Self::Io(err),
|
||||
other => Self::Notify(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<PoisonError<T>> for Error {
|
||||
fn from(_err: PoisonError<T>) -> Self {
|
||||
Self::PoisonedLock
|
||||
}
|
||||
}
|
||||
|
||||
impl From<clearscreen::Error> for Error {
|
||||
fn from(err: clearscreen::Error) -> Self {
|
||||
match err {
|
||||
clearscreen::Error::Io(err) => Self::Io(err),
|
||||
other => Self::ClearScreen(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let (error_type, error) = match self {
|
||||
Self::Canonicalization(path, err) => (
|
||||
"Path",
|
||||
format!("couldn't canonicalize '{}':\n{}", path, err),
|
||||
),
|
||||
Self::Generic(err) => ("", err.clone()),
|
||||
Self::Glob(err) => ("Globset", err.to_string()),
|
||||
Self::Io(err) => ("I/O", err.to_string()),
|
||||
Self::Notify(err) => ("Notify", err.to_string()),
|
||||
Self::PoisonedLock => ("Internal", "poisoned lock".to_string()),
|
||||
Self::ClearScreen(err) => ("ClearScreen", err.to_string()),
|
||||
};
|
||||
|
||||
write!(f, "{} error: {}", error_type, error)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt::Display::fmt(self, f)
|
||||
}
|
||||
/// Error received when creating a filesystem watcher fails.
|
||||
#[error("{kind:?} watcher failed to instantiate: {err}")]
|
||||
#[diagnostic(
|
||||
code(watchexec::runtime::fs_watcher_error),
|
||||
help("perhaps retry with the poll watcher"),
|
||||
)]
|
||||
FsWatcherCreate { kind: Watcher, #[source] err: notify::Error },
|
||||
|
||||
/// Error received when reading a filesystem event fails.
|
||||
#[error("{kind:?} watcher received an event that we could not read: {err}")]
|
||||
#[diagnostic(
|
||||
code(watchexec::runtime::fs_watcher_event),
|
||||
)]
|
||||
FsWatcherEvent { kind: Watcher, #[source] err: notify::Error },
|
||||
|
||||
/// Error received when adding to the pathset for the filesystem watcher fails.
|
||||
#[error("while adding {path:?} to the {kind:?} watcher: {err}")]
|
||||
#[diagnostic(
|
||||
code(watchexec::runtime::fs_watcher_path_add),
|
||||
)]
|
||||
FsWatcherPathAdd { path: PathBuf, kind: Watcher, #[source] err: notify::Error },
|
||||
|
||||
/// Error received when removing from the pathset for the filesystem watcher fails.
|
||||
#[error("while removing {path:?} from the {kind:?} watcher: {err}")]
|
||||
#[diagnostic(
|
||||
code(watchexec::runtime::fs_watcher_path_remove),
|
||||
)]
|
||||
FsWatcherPathRemove { path: PathBuf, kind: Watcher, #[source] err: notify::Error },
|
||||
|
||||
/// Error received when an event cannot be sent to the event channel.
|
||||
#[error("cannot send event from {ctx}: {err}")]
|
||||
#[diagnostic(code(watchexec::runtime::event_channel_send))]
|
||||
EventChannelSend { ctx: &'static str, #[source] err: mpsc::error::TrySendError<Event> },
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
//! Fundamentally, events in watchexec have three purposes:
|
||||
//!
|
||||
//! 1. To trigger the launch, restart, or other interruption of a process;
|
||||
//! 2. To be filtered upon according to whatever set of criteria is desired;
|
||||
//! 3. To carry information about what caused the event, which may be provided to the process.
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
/// An event, as far as watchexec cares about.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Event {
|
||||
pub particulars: Vec<Particle>,
|
||||
pub metadata: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
/// Something which can be used to filter an event.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Particle {
|
||||
Time(NaiveDateTime),
|
||||
Path(PathBuf),
|
||||
Source(Source),
|
||||
Process(u32),
|
||||
}
|
||||
|
||||
/// The general origin of the event.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Source {
|
||||
Filesystem,
|
||||
Keyboard,
|
||||
Mouse,
|
||||
Time,
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
use std::{collections::{HashMap, HashSet}, path::PathBuf};
|
||||
|
||||
use tokio::{sync::{mpsc, watch}};
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use crate::{error::{CriticalError, RuntimeError}, event::{Event, Particle, Source}};
|
||||
|
||||
/// What kind of filesystem watcher to use.
|
||||
///
|
||||
/// For now only native and poll watchers are supported. In the future there may be additional
|
||||
/// watchers available on some platforms.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Watcher {
|
||||
Native,
|
||||
Poll,
|
||||
}
|
||||
|
||||
impl Default for Watcher {
|
||||
fn default() -> Self {
|
||||
Self::Native
|
||||
}
|
||||
}
|
||||
|
||||
impl Watcher {
|
||||
fn create(self, f: impl notify::EventFn) -> Result<Box<dyn notify::Watcher>, RuntimeError> {
|
||||
match self {
|
||||
Self::Native => notify::RecommendedWatcher::new(f).map(|w| Box::new(w) as Box<dyn notify::Watcher>),
|
||||
Self::Poll => notify::PollWatcher::new(f).map(|w| Box::new(w) as Box<dyn notify::Watcher>),
|
||||
}.map_err(|err| RuntimeError::FsWatcherCreate { kind: self, err })
|
||||
}
|
||||
}
|
||||
|
||||
/// The working data set of the filesystem worker.
|
||||
///
|
||||
/// This is marked non-exhaustive so new configuration can be added without breaking.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct WorkingData {
|
||||
pub pathset: Vec<PathBuf>,
|
||||
pub watcher: Watcher,
|
||||
}
|
||||
|
||||
/// Launch a filesystem event worker.
|
||||
///
|
||||
/// This only does a bare minimum of setup; to actually start the work, you need to set a non-empty pathset on the
|
||||
/// [`WorkingData`] with the [`watch`] channel.
|
||||
pub async fn worker(
|
||||
mut working: watch::Receiver<WorkingData>,
|
||||
errors: mpsc::Sender<RuntimeError>,
|
||||
events: mpsc::Sender<Event>,
|
||||
) -> Result<(), CriticalError> {
|
||||
debug!("launching filesystem worker");
|
||||
|
||||
let mut watcher_type = Watcher::default();
|
||||
let mut watcher: Option<Box<dyn notify::Watcher>> = None;
|
||||
let mut pathset: HashSet<PathBuf> = HashSet::new();
|
||||
|
||||
while working.changed().await.is_ok() {
|
||||
// In separate scope so we drop the working read lock as early as we can
|
||||
let (new_watcher, to_watch, to_drop) = {
|
||||
let data = working.borrow();
|
||||
trace!(?data, "filesystem worker got a working data change");
|
||||
|
||||
if data.pathset.is_empty() {
|
||||
trace!("no more watched paths, dropping watcher");
|
||||
watcher.take();
|
||||
pathset.drain();
|
||||
continue;
|
||||
}
|
||||
|
||||
if watcher.is_none() || watcher_type != data.watcher {
|
||||
pathset.drain();
|
||||
|
||||
(Some(data.watcher), data.pathset.clone(), Vec::new())
|
||||
} else {
|
||||
let mut to_watch = Vec::with_capacity(data.pathset.len());
|
||||
let mut to_drop = Vec::with_capacity(pathset.len());
|
||||
for path in data.pathset.iter() {
|
||||
if !pathset.contains(path) {
|
||||
to_watch.push(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for path in pathset.iter() {
|
||||
if !data.pathset.contains(path) {
|
||||
to_drop.push(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
(None, to_watch, to_drop)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(kind) = new_watcher {
|
||||
debug!(?kind, "creating new watcher");
|
||||
let n_errors = errors.clone();
|
||||
let n_events = events.clone();
|
||||
match kind.create(move |nev: Result<notify::Event, notify::Error> | {
|
||||
trace!(event = ?nev, "receiving possible event from watcher");
|
||||
|
||||
match nev {
|
||||
Err(err) => {
|
||||
n_errors.try_send(RuntimeError::FsWatcherEvent { kind, err }).ok();
|
||||
},
|
||||
|
||||
Ok(nev) => {
|
||||
let mut particulars = Vec::with_capacity(4);
|
||||
particulars.push(Particle::Source(Source::Filesystem));
|
||||
|
||||
for path in nev.paths {
|
||||
particulars.push(Particle::Path(path));
|
||||
}
|
||||
|
||||
if let Some(pid) = nev.attrs.process_id() {
|
||||
particulars.push(Particle::Process(pid));
|
||||
}
|
||||
|
||||
let ev = Event {
|
||||
particulars,
|
||||
metadata: HashMap::new(), // TODO
|
||||
};
|
||||
|
||||
trace!(event = ?ev, "processed notify event into watchexec event");
|
||||
if let Err(err) = n_events.try_send(ev) {
|
||||
n_errors.try_send(RuntimeError::EventChannelSend {
|
||||
ctx: "fs watcher",
|
||||
err,
|
||||
}).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Ok(w) => {
|
||||
watcher.insert(w);
|
||||
watcher_type = kind;
|
||||
},
|
||||
Err(e) => {
|
||||
errors.send(e).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(w) = watcher.as_mut() {
|
||||
debug!(?to_watch, ?to_drop, "applying changes to the watcher");
|
||||
|
||||
for path in to_drop {
|
||||
trace!(?path, "removing path from the watcher");
|
||||
if let Err(err) = w.unwatch(&path) {
|
||||
errors.send(RuntimeError::FsWatcherPathRemove { path, kind: watcher_type, err }).await?;
|
||||
} else {
|
||||
pathset.remove(&path);
|
||||
}
|
||||
}
|
||||
|
||||
for path in to_watch {
|
||||
trace!(?path, "adding path to the watcher");
|
||||
if let Err(err) = w.watch(&path, notify::RecursiveMode::Recursive) {
|
||||
errors.send(RuntimeError::FsWatcherPathAdd { path, kind: watcher_type, err }).await?;
|
||||
} else {
|
||||
pathset.insert(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,363 +0,0 @@
|
|||
use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
|
||||
use log::debug;
|
||||
|
||||
use std::borrow::ToOwned;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub struct Gitignore {
|
||||
files: Vec<GitignoreFile>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
GlobSet(globset::Error),
|
||||
Io(io::Error),
|
||||
}
|
||||
|
||||
struct GitignoreFile {
|
||||
set: GlobSet,
|
||||
patterns: Vec<Pattern>,
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
struct Pattern {
|
||||
pattern: String,
|
||||
pattern_type: PatternType,
|
||||
anchored: bool,
|
||||
}
|
||||
|
||||
enum PatternType {
|
||||
Ignore,
|
||||
Whitelist,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum MatchResult {
|
||||
Ignore,
|
||||
Whitelist,
|
||||
None,
|
||||
}
|
||||
|
||||
pub fn load(paths: &[PathBuf]) -> Gitignore {
|
||||
let mut files = vec![];
|
||||
|
||||
for path in paths {
|
||||
let mut top_level_git_dir = None;
|
||||
let mut p = Some(path.as_path());
|
||||
|
||||
while let Some(ref current) = p {
|
||||
debug!("Looking in {:?} for a .git directory", current);
|
||||
|
||||
// Stop if we see a .git directory
|
||||
if let Ok(metadata) = current.join(".git").metadata() {
|
||||
if metadata.is_dir() {
|
||||
top_level_git_dir = Some(*current);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
p = current.parent();
|
||||
}
|
||||
|
||||
if let Some(root) = top_level_git_dir {
|
||||
debug!("Found the top level git directory: {:?}", root);
|
||||
// scan in subdirectories
|
||||
for entry in WalkDir::new(root)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|e| e.file_type().is_file())
|
||||
.filter(|e| e.file_name() == ".gitignore")
|
||||
{
|
||||
let gitignore_path = entry.path();
|
||||
if let Ok(f) = GitignoreFile::new(gitignore_path) {
|
||||
debug!("Loaded {:?}", gitignore_path);
|
||||
files.push(f);
|
||||
} else {
|
||||
debug!("Unable to load {:?}", gitignore_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// p.pop();
|
||||
}
|
||||
|
||||
Gitignore::new(files)
|
||||
}
|
||||
|
||||
impl Gitignore {
|
||||
const fn new(files: Vec<GitignoreFile>) -> Self {
|
||||
Self { files }
|
||||
}
|
||||
|
||||
pub fn is_excluded(&self, path: &Path) -> bool {
|
||||
let mut applicable_files: Vec<&GitignoreFile> = self
|
||||
.files
|
||||
.iter()
|
||||
.filter(|f| path.starts_with(&f.root))
|
||||
.collect();
|
||||
applicable_files.sort_by_key(|f| f.root_len());
|
||||
|
||||
// TODO: add user gitignores
|
||||
|
||||
let mut result = MatchResult::None;
|
||||
|
||||
for file in applicable_files {
|
||||
match file.matches(path) {
|
||||
MatchResult::Ignore => result = MatchResult::Ignore,
|
||||
MatchResult::Whitelist => result = MatchResult::Whitelist,
|
||||
MatchResult::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
result == MatchResult::Ignore
|
||||
}
|
||||
}
|
||||
|
||||
impl GitignoreFile {
|
||||
pub fn new(path: &Path) -> Result<Self, Error> {
|
||||
let mut file = fs::File::open(path)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
|
||||
let lines: Vec<_> = contents.lines().collect();
|
||||
let root = path.parent().expect("gitignore file is at filesystem root");
|
||||
|
||||
Self::from_strings(&lines, root)
|
||||
}
|
||||
|
||||
pub fn from_strings(strs: &[&str], root: &Path) -> Result<Self, Error> {
|
||||
let mut builder = GlobSetBuilder::new();
|
||||
let mut patterns = vec![];
|
||||
|
||||
let parsed_patterns = Self::parse(strs);
|
||||
for p in parsed_patterns {
|
||||
let mut pat = p.pattern.clone();
|
||||
if !p.anchored && !pat.starts_with("**/") {
|
||||
pat = "**/".to_string() + &pat;
|
||||
}
|
||||
|
||||
if !pat.ends_with("/**") {
|
||||
pat += "/**";
|
||||
}
|
||||
|
||||
let glob = GlobBuilder::new(&pat).literal_separator(true).build()?;
|
||||
|
||||
builder.add(glob);
|
||||
patterns.push(p);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
set: builder.build()?,
|
||||
patterns,
|
||||
root: root.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn is_excluded(&self, path: &Path) -> bool {
|
||||
self.matches(path) == MatchResult::Ignore
|
||||
}
|
||||
|
||||
fn matches(&self, path: &Path) -> MatchResult {
|
||||
if let Ok(stripped) = path.strip_prefix(&self.root) {
|
||||
let matches = self.set.matches(stripped);
|
||||
if let Some(i) = matches.iter().rev().next() {
|
||||
let pattern = &self.patterns[*i];
|
||||
return match pattern.pattern_type {
|
||||
PatternType::Whitelist => MatchResult::Whitelist,
|
||||
PatternType::Ignore => MatchResult::Ignore,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
MatchResult::None
|
||||
}
|
||||
|
||||
pub fn root_len(&self) -> usize {
|
||||
self.root.as_os_str().len()
|
||||
}
|
||||
|
||||
fn parse(contents: &[&str]) -> Vec<Pattern> {
|
||||
contents
|
||||
.iter()
|
||||
.filter_map(|l| {
|
||||
if !l.is_empty() && !l.starts_with('#') {
|
||||
Some(Pattern::parse(l))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
fn parse(pattern: &str) -> Self {
|
||||
let mut normalized = String::from(pattern);
|
||||
|
||||
let pattern_type = if normalized.starts_with('!') {
|
||||
normalized.remove(0);
|
||||
PatternType::Whitelist
|
||||
} else {
|
||||
PatternType::Ignore
|
||||
};
|
||||
|
||||
let anchored = if normalized.starts_with('/') {
|
||||
normalized.remove(0);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if normalized.ends_with('/') {
|
||||
normalized.pop();
|
||||
}
|
||||
|
||||
if normalized.starts_with("\\#") || normalized.starts_with("\\!") {
|
||||
normalized.remove(0);
|
||||
}
|
||||
|
||||
Self {
|
||||
pattern: normalized,
|
||||
pattern_type,
|
||||
anchored,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<globset::Error> for Error {
|
||||
fn from(error: globset::Error) -> Self {
|
||||
Self::GlobSet(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(error: io::Error) -> Self {
|
||||
Self::Io(error)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::GitignoreFile;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn base_dir() -> PathBuf {
|
||||
PathBuf::from("/home/user/dir")
|
||||
}
|
||||
|
||||
fn build_gitignore(pattern: &str) -> GitignoreFile {
|
||||
GitignoreFile::from_strings(&[pattern], &base_dir()).expect("test gitignore file invalid")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_exact() {
|
||||
let file = build_gitignore("Cargo.toml");
|
||||
|
||||
assert!(file.is_excluded(&base_dir().join("Cargo.toml")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_match() {
|
||||
let file = build_gitignore("Cargo.toml");
|
||||
|
||||
assert!(!file.is_excluded(&base_dir().join("src").join("main.rs")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_simple_wildcard() {
|
||||
let file = build_gitignore("targ*");
|
||||
|
||||
assert!(file.is_excluded(&base_dir().join("target")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_subdir_exact() {
|
||||
let file = build_gitignore("target");
|
||||
|
||||
assert!(file.is_excluded(&base_dir().join("target/")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_subdir() {
|
||||
let file = build_gitignore("target");
|
||||
|
||||
assert!(file.is_excluded(&base_dir().join("target").join("file")));
|
||||
assert!(file.is_excluded(&base_dir().join("target").join("subdir").join("file")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wildcard_with_dir() {
|
||||
let file = build_gitignore("target/f*");
|
||||
|
||||
assert!(file.is_excluded(&base_dir().join("target").join("file")));
|
||||
assert!(!file.is_excluded(&base_dir().join("target").join("subdir").join("file")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leading_slash() {
|
||||
let file = build_gitignore("/*.c");
|
||||
|
||||
assert!(file.is_excluded(&base_dir().join("cat-file.c")));
|
||||
assert!(!file.is_excluded(&base_dir().join("mozilla-sha1").join("sha1.c")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leading_double_wildcard() {
|
||||
let file = build_gitignore("**/foo");
|
||||
|
||||
assert!(file.is_excluded(&base_dir().join("foo")));
|
||||
assert!(file.is_excluded(&base_dir().join("target").join("foo")));
|
||||
assert!(file.is_excluded(&base_dir().join("target").join("subdir").join("foo")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_double_wildcard() {
|
||||
let file = build_gitignore("abc/**");
|
||||
|
||||
assert!(!file.is_excluded(&base_dir().join("def").join("foo")));
|
||||
assert!(file.is_excluded(&base_dir().join("abc").join("foo")));
|
||||
assert!(file.is_excluded(&base_dir().join("abc").join("subdir").join("foo")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandwiched_double_wildcard() {
|
||||
let file = build_gitignore("a/**/b");
|
||||
|
||||
assert!(file.is_excluded(&base_dir().join("a").join("b")));
|
||||
assert!(file.is_excluded(&base_dir().join("a").join("x").join("b")));
|
||||
assert!(file.is_excluded(&base_dir().join("a").join("x").join("y").join("b")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_file_never_excludes() {
|
||||
let file =
|
||||
GitignoreFile::from_strings(&[], &base_dir()).expect("test gitignore file invalid");
|
||||
|
||||
assert!(!file.is_excluded(&base_dir().join("target")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checks_all_patterns() {
|
||||
let patterns = vec!["target", "target2"];
|
||||
let file = GitignoreFile::from_strings(&patterns, &base_dir())
|
||||
.expect("test gitignore file invalid");
|
||||
|
||||
assert!(file.is_excluded(&base_dir().join("target").join("foo.txt")));
|
||||
assert!(file.is_excluded(&base_dir().join("target2").join("bar.txt")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_negative_patterns() {
|
||||
let patterns = vec!["target", "!target/foo.txt"];
|
||||
let file = GitignoreFile::from_strings(&patterns, &base_dir())
|
||||
.expect("test gitignore file invalid");
|
||||
|
||||
assert!(!file.is_excluded(&base_dir().join("target").join("foo.txt")));
|
||||
assert!(file.is_excluded(&base_dir().join("target").join("blah.txt")));
|
||||
}
|
||||
}
|
|
@ -1,367 +0,0 @@
|
|||
use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
|
||||
use log::debug;
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub struct Ignore {
|
||||
files: Vec<IgnoreFile>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
GlobSet(globset::Error),
|
||||
Io(io::Error),
|
||||
}
|
||||
|
||||
struct IgnoreFile {
|
||||
set: GlobSet,
|
||||
patterns: Vec<Pattern>,
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
struct Pattern {
|
||||
pattern: String,
|
||||
pattern_type: PatternType,
|
||||
anchored: bool,
|
||||
}
|
||||
|
||||
enum PatternType {
|
||||
Ignore,
|
||||
Whitelist,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum MatchResult {
|
||||
Ignore,
|
||||
Whitelist,
|
||||
None,
|
||||
}
|
||||
|
||||
pub fn load(paths: &[PathBuf]) -> Ignore {
|
||||
let mut files = vec![];
|
||||
let mut checked_dirs = HashSet::new();
|
||||
|
||||
for path in paths {
|
||||
let mut p = path.to_owned();
|
||||
|
||||
// walk up to root
|
||||
// FIXME: this makes zero sense and should be removed
|
||||
// but that would be a breaking change
|
||||
loop {
|
||||
if !checked_dirs.contains(&p) {
|
||||
checked_dirs.insert(p.clone());
|
||||
|
||||
let ignore_path = p.join(".ignore");
|
||||
if ignore_path.exists() {
|
||||
if let Ok(f) = IgnoreFile::new(&ignore_path) {
|
||||
debug!("Loaded {:?}", ignore_path);
|
||||
files.push(f);
|
||||
} else {
|
||||
debug!("Unable to load {:?}", ignore_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.parent().is_none() {
|
||||
break;
|
||||
}
|
||||
|
||||
p.pop();
|
||||
}
|
||||
|
||||
//also look in subfolders
|
||||
for entry in WalkDir::new(path)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|e| e.file_type().is_file())
|
||||
.filter(|e| e.file_name() == ".ignore")
|
||||
{
|
||||
let ignore_path = entry.path();
|
||||
if let Ok(f) = IgnoreFile::new(ignore_path) {
|
||||
debug!("Loaded {:?}", ignore_path);
|
||||
files.push(f);
|
||||
} else {
|
||||
debug!("Unable to load {:?}", ignore_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ignore::new(files)
|
||||
}
|
||||
|
||||
impl Ignore {
|
||||
const fn new(files: Vec<IgnoreFile>) -> Self {
|
||||
Self { files }
|
||||
}
|
||||
|
||||
pub fn is_excluded(&self, path: &Path) -> bool {
|
||||
let mut applicable_files: Vec<&IgnoreFile> = self
|
||||
.files
|
||||
.iter()
|
||||
.filter(|f| path.starts_with(&f.root))
|
||||
.collect();
|
||||
applicable_files.sort_by_key(|f| f.root_len());
|
||||
|
||||
// TODO: add user ignores
|
||||
|
||||
let mut result = MatchResult::None;
|
||||
|
||||
for file in applicable_files {
|
||||
match file.matches(path) {
|
||||
MatchResult::Ignore => result = MatchResult::Ignore,
|
||||
MatchResult::Whitelist => result = MatchResult::Whitelist,
|
||||
MatchResult::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
result == MatchResult::Ignore
|
||||
}
|
||||
}
|
||||
|
||||
impl IgnoreFile {
|
||||
pub fn new(path: &Path) -> Result<Self, Error> {
|
||||
let mut file = fs::File::open(path)?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
|
||||
let lines: Vec<_> = contents.lines().collect();
|
||||
let root = path.parent().expect("ignore file is at filesystem root");
|
||||
|
||||
Self::from_strings(&lines, root)
|
||||
}
|
||||
|
||||
pub fn from_strings(strs: &[&str], root: &Path) -> Result<Self, Error> {
|
||||
let mut builder = GlobSetBuilder::new();
|
||||
let mut patterns = vec![];
|
||||
|
||||
let parsed_patterns = Self::parse(strs);
|
||||
for p in parsed_patterns {
|
||||
let mut pat = p.pattern.clone();
|
||||
if !p.anchored && !pat.starts_with("**/") {
|
||||
pat = "**/".to_string() + &pat;
|
||||
}
|
||||
|
||||
if !pat.ends_with("/**") {
|
||||
pat += "/**";
|
||||
}
|
||||
|
||||
let glob = GlobBuilder::new(&pat).literal_separator(true).build()?;
|
||||
|
||||
builder.add(glob);
|
||||
patterns.push(p);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
set: builder.build()?,
|
||||
patterns,
|
||||
root: root.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn is_excluded(&self, path: &Path) -> bool {
|
||||
self.matches(path) == MatchResult::Ignore
|
||||
}
|
||||
|
||||
fn matches(&self, path: &Path) -> MatchResult {
|
||||
if let Ok(stripped) = path.strip_prefix(&self.root) {
|
||||
let matches = self.set.matches(stripped);
|
||||
if let Some(i) = matches.iter().rev().next() {
|
||||
let pattern = &self.patterns[*i];
|
||||
return match pattern.pattern_type {
|
||||
PatternType::Whitelist => MatchResult::Whitelist,
|
||||
PatternType::Ignore => MatchResult::Ignore,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
MatchResult::None
|
||||
}
|
||||
|
||||
pub fn root_len(&self) -> usize {
|
||||
self.root.as_os_str().len()
|
||||
}
|
||||
|
||||
fn parse(contents: &[&str]) -> Vec<Pattern> {
|
||||
contents
|
||||
.iter()
|
||||
.filter_map(|l| {
|
||||
if !l.is_empty() && !l.starts_with('#') {
|
||||
Some(Pattern::parse(l))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
fn parse(pattern: &str) -> Self {
|
||||
let mut normalized = String::from(pattern);
|
||||
|
||||
let pattern_type = if normalized.starts_with('!') {
|
||||
normalized.remove(0);
|
||||
PatternType::Whitelist
|
||||
} else {
|
||||
PatternType::Ignore
|
||||
};
|
||||
|
||||
let anchored = if normalized.starts_with('/') {
|
||||
normalized.remove(0);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if normalized.ends_with('/') {
|
||||
normalized.pop();
|
||||
}
|
||||
|
||||
if normalized.starts_with("\\#") || normalized.starts_with("\\!") {
|
||||
normalized.remove(0);
|
||||
}
|
||||
|
||||
Self {
|
||||
pattern: normalized,
|
||||
pattern_type,
|
||||
anchored,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<globset::Error> for Error {
|
||||
fn from(error: globset::Error) -> Self {
|
||||
Self::GlobSet(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(error: io::Error) -> Self {
|
||||
Self::Io(error)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::IgnoreFile;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn base_dir() -> PathBuf {
|
||||
PathBuf::from("/home/user/dir")
|
||||
}
|
||||
|
||||
fn build_ignore(pattern: &str) -> IgnoreFile {
|
||||
IgnoreFile::from_strings(&[pattern], &base_dir()).expect("test ignore file invalid")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_exact() {
|
||||
let file = build_ignore("Cargo.toml");
|
||||
|
||||
assert!(file.is_excluded(&base_dir().join("Cargo.toml")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_match() {
|
||||
let file = build_ignore("Cargo.toml");
|
||||
|
||||
assert!(!file.is_excluded(&base_dir().join("src").join("main.rs")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_simple_wildcard() {
|
||||
let file = build_ignore("targ*");
|
||||
|
||||
assert!(file.is_excluded(&base_dir().join("target")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_subdir_exact() {
|
||||
let file = build_ignore("target");
|
||||
|
||||
assert!(file.is_excluded(&base_dir().join("target/")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_subdir() {
|
||||
let file = build_ignore("target");
|
||||
|
||||
assert!(file.is_excluded(&base_dir().join("target").join("file")));
|
||||
assert!(file.is_excluded(&base_dir().join("target").join("subdir").join("file")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wildcard_with_dir() {
|
||||
let file = build_ignore("target/f*");
|
||||
|
||||
assert!(file.is_excluded(&base_dir().join("target").join("file")));
|
||||
assert!(!file.is_excluded(&base_dir().join("target").join("subdir").join("file")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leading_slash() {
|
||||
let file = build_ignore("/*.c");
|
||||
|
||||
assert!(file.is_excluded(&base_dir().join("cat-file.c")));
|
||||
assert!(!file.is_excluded(&base_dir().join("mozilla-sha1").join("sha1.c")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leading_double_wildcard() {
|
||||
let file = build_ignore("**/foo");
|
||||
|
||||
assert!(file.is_excluded(&base_dir().join("foo")));
|
||||
assert!(file.is_excluded(&base_dir().join("target").join("foo")));
|
||||
assert!(file.is_excluded(&base_dir().join("target").join("subdir").join("foo")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_double_wildcard() {
|
||||
let file = build_ignore("abc/**");
|
||||
|
||||
assert!(!file.is_excluded(&base_dir().join("def").join("foo")));
|
||||
assert!(file.is_excluded(&base_dir().join("abc").join("foo")));
|
||||
assert!(file.is_excluded(&base_dir().join("abc").join("subdir").join("foo")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandwiched_double_wildcard() {
|
||||
let file = build_ignore("a/**/b");
|
||||
|
||||
assert!(file.is_excluded(&base_dir().join("a").join("b")));
|
||||
assert!(file.is_excluded(&base_dir().join("a").join("x").join("b")));
|
||||
assert!(file.is_excluded(&base_dir().join("a").join("x").join("y").join("b")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_file_never_excludes() {
|
||||
let file = IgnoreFile::from_strings(&[], &base_dir()).expect("test ignore file invalid");
|
||||
|
||||
assert!(!file.is_excluded(&base_dir().join("target")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checks_all_patterns() {
|
||||
let patterns = vec!["target", "target2"];
|
||||
let file =
|
||||
IgnoreFile::from_strings(&patterns, &base_dir()).expect("test ignore file invalid");
|
||||
|
||||
assert!(file.is_excluded(&base_dir().join("target").join("foo.txt")));
|
||||
assert!(file.is_excluded(&base_dir().join("target2").join("bar.txt")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_whitelisting() {
|
||||
let patterns = vec!["target", "!target/foo.txt"];
|
||||
let file =
|
||||
IgnoreFile::from_strings(&patterns, &base_dir()).expect("test ignore file invalid");
|
||||
|
||||
assert!(!file.is_excluded(&base_dir().join("target").join("foo.txt")));
|
||||
assert!(file.is_excluded(&base_dir().join("target").join("blah.txt")));
|
||||
}
|
||||
}
|
|
@ -1,24 +1,20 @@
|
|||
//! [Watchexec]: the library
|
||||
//! Watchexec: a library for utilities and programs which respond to events;
|
||||
//! file changes, human interaction, and more.
|
||||
//!
|
||||
//! From version 1.16.0, semver applies!
|
||||
//! Also see the CLI tool: https://watchexec.github.io/
|
||||
//!
|
||||
//! [Watchexec]: https://github.com/watchexec/watchexec
|
||||
//! This library is powered by [Tokio](https://tokio.rs), minimum version 1.10.
|
||||
//!
|
||||
//! The main way to use this crate involves constructing a [`Handler`] and running it.
|
||||
//!
|
||||
//! This crate does not itself use `unsafe`. However, it depends on a number of libraries which do,
|
||||
//! most because they interact with the operating system.
|
||||
|
||||
#![doc(html_favicon_url = "https://watchexec.github.io/logo:watchexec.svg")]
|
||||
#![doc(html_logo_url = "https://watchexec.github.io/logo:watchexec.svg")]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
mod gitignore;
|
||||
mod ignore;
|
||||
mod notification_filter;
|
||||
pub mod pathop;
|
||||
mod paths;
|
||||
pub mod run;
|
||||
mod shell;
|
||||
mod signal;
|
||||
mod watcher;
|
||||
|
||||
pub use run::{run, watch, Handler};
|
||||
pub use shell::Shell;
|
||||
pub mod event;
|
||||
mod fs;
|
||||
|
|
|
@ -1,160 +0,0 @@
|
|||
use crate::error;
|
||||
use crate::gitignore::Gitignore;
|
||||
use crate::ignore::Ignore;
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
use log::debug;
|
||||
use std::path::Path;
|
||||
|
||||
pub struct NotificationFilter {
|
||||
filters: GlobSet,
|
||||
filter_count: usize,
|
||||
ignores: GlobSet,
|
||||
gitignore_files: Gitignore,
|
||||
ignore_files: Ignore,
|
||||
}
|
||||
|
||||
impl NotificationFilter {
|
||||
pub fn new(
|
||||
filters: &[String],
|
||||
ignores: &[String],
|
||||
gitignore_files: Gitignore,
|
||||
ignore_files: Ignore,
|
||||
) -> error::Result<Self> {
|
||||
let mut filter_set_builder = GlobSetBuilder::new();
|
||||
for f in filters {
|
||||
filter_set_builder.add(Glob::new(f)?);
|
||||
debug!("Adding filter: \"{}\"", f);
|
||||
}
|
||||
|
||||
let mut ignore_set_builder = GlobSetBuilder::new();
|
||||
for i in ignores {
|
||||
let mut ignore_path = Path::new(i).to_path_buf();
|
||||
if ignore_path.is_relative() && !i.starts_with('*') {
|
||||
ignore_path = Path::new("**").join(&ignore_path);
|
||||
}
|
||||
if !i.ends_with('*') {
|
||||
ignore_path = ignore_path.join("**");
|
||||
}
|
||||
let pattern = ignore_path
|
||||
.to_str()
|
||||
.expect("corrupted memory (string -> path -> string)");
|
||||
ignore_set_builder.add(Glob::new(pattern)?);
|
||||
debug!("Adding ignore: \"{}\"", pattern);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
filters: filter_set_builder.build()?,
|
||||
filter_count: filters.len(),
|
||||
ignores: ignore_set_builder.build()?,
|
||||
gitignore_files,
|
||||
ignore_files,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_excluded(&self, path: &Path) -> bool {
|
||||
if self.ignores.is_match(path) {
|
||||
debug!("Ignoring {:?}: matched ignore filter", path);
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.filters.is_match(path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.ignore_files.is_excluded(path) {
|
||||
debug!("Ignoring {:?}: matched ignore file", path);
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.gitignore_files.is_excluded(path) {
|
||||
debug!("Ignoring {:?}: matched gitignore file", path);
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.filter_count > 0 {
|
||||
debug!("Ignoring {:?}: did not match any given filters", path);
|
||||
}
|
||||
|
||||
self.filter_count > 0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::NotificationFilter;
|
||||
use crate::gitignore;
|
||||
use crate::ignore;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn test_allows_everything_by_default() {
|
||||
let filter = NotificationFilter::new(&[], &[], gitignore::load(&[]), ignore::load(&[]))
|
||||
.expect("test filter errors");
|
||||
|
||||
assert!(!filter.is_excluded(Path::new("foo")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filename() {
|
||||
let filter = NotificationFilter::new(
|
||||
&[],
|
||||
&["test.json".into()],
|
||||
gitignore::load(&[]),
|
||||
ignore::load(&[]),
|
||||
)
|
||||
.expect("test filter errors");
|
||||
|
||||
assert!(filter.is_excluded(Path::new("/path/to/test.json")));
|
||||
assert!(filter.is_excluded(Path::new("test.json")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_filters() {
|
||||
let filters = &["*.rs".into(), "*.toml".into()];
|
||||
let filter = NotificationFilter::new(filters, &[], gitignore::load(&[]), ignore::load(&[]))
|
||||
.expect("test filter errors");
|
||||
|
||||
assert!(!filter.is_excluded(Path::new("hello.rs")));
|
||||
assert!(!filter.is_excluded(Path::new("Cargo.toml")));
|
||||
assert!(filter.is_excluded(Path::new("README.md")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_ignores() {
|
||||
let ignores = &["*.rs".into(), "*.toml".into()];
|
||||
let filter = NotificationFilter::new(&[], ignores, gitignore::load(&[]), ignore::load(&[]))
|
||||
.expect("test filter errors");
|
||||
|
||||
assert!(filter.is_excluded(Path::new("hello.rs")));
|
||||
assert!(filter.is_excluded(Path::new("Cargo.toml")));
|
||||
assert!(!filter.is_excluded(Path::new("README.md")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ignores_take_precedence() {
|
||||
let ignores = &["*.rs".into(), "*.toml".into()];
|
||||
let filter =
|
||||
NotificationFilter::new(ignores, ignores, gitignore::load(&[]), ignore::load(&[]))
|
||||
.expect("test filter errors");
|
||||
|
||||
assert!(filter.is_excluded(Path::new("hello.rs")));
|
||||
assert!(filter.is_excluded(Path::new("Cargo.toml")));
|
||||
assert!(filter.is_excluded(Path::new("README.md")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recursive_directory_ignore() {
|
||||
let ignores = &["target".into()];
|
||||
let filter = NotificationFilter::new(&[], ignores, gitignore::load(&[]), ignore::load(&[]))
|
||||
.expect("test filter errors");
|
||||
|
||||
assert!(filter.is_excluded(Path::new("target")));
|
||||
// Make sure that sub-directories/-files are recursively ignored.
|
||||
assert!(filter.is_excluded(Path::new("target/rls")));
|
||||
assert!(filter.is_excluded(Path::new("target/rls/debug")));
|
||||
// Assert that files containing subsets of the path are not ignored.
|
||||
assert!(!filter.is_excluded(Path::new("target-file")));
|
||||
assert!(!filter.is_excluded(Path::new("hello.rs")));
|
||||
assert!(!filter.is_excluded(Path::new("Cargo.toml")));
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
use notify::op;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Info about a path and its corresponding `notify` event
|
||||
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
||||
pub struct PathOp {
|
||||
pub path: PathBuf,
|
||||
pub op: Option<op::Op>,
|
||||
pub cookie: Option<u32>,
|
||||
}
|
||||
|
||||
impl PathOp {
|
||||
pub fn new(path: &Path, op: Option<op::Op>, cookie: Option<u32>) -> Self {
|
||||
Self {
|
||||
path: path.to_path_buf(),
|
||||
op,
|
||||
cookie,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_create(op_: op::Op) -> bool {
|
||||
op_.contains(op::CREATE)
|
||||
}
|
||||
|
||||
pub const fn is_remove(op_: op::Op) -> bool {
|
||||
op_.contains(op::REMOVE)
|
||||
}
|
||||
|
||||
pub const fn is_rename(op_: op::Op) -> bool {
|
||||
op_.contains(op::RENAME)
|
||||
}
|
||||
|
||||
pub fn is_write(op_: op::Op) -> bool {
|
||||
let mut write_or_close_write = op::WRITE;
|
||||
write_or_close_write.toggle(op::CLOSE_WRITE);
|
||||
op_.intersects(write_or_close_write)
|
||||
}
|
||||
|
||||
pub const fn is_meta(op_: op::Op) -> bool {
|
||||
op_.contains(op::CHMOD)
|
||||
}
|
||||
}
|
289
lib/src/paths.rs
289
lib/src/paths.rs
|
@ -1,289 +0,0 @@
|
|||
use crate::pathop::PathOp;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
/// Collect `PathOp` details into op-categories to pass onto the exec'd command as env-vars
|
||||
///
|
||||
/// `WRITTEN` -> `notify::ops::WRITE`, `notify::ops::CLOSE_WRITE`
|
||||
/// `META_CHANGED` -> `notify::ops::CHMOD`
|
||||
/// `REMOVED` -> `notify::ops::REMOVE`
|
||||
/// `CREATED` -> `notify::ops::CREATE`
|
||||
/// `RENAMED` -> `notify::ops::RENAME`
|
||||
pub fn collect_path_env_vars(pathops: &[PathOp]) -> Vec<(String, String)> {
|
||||
#[cfg(target_family = "unix")]
|
||||
const ENV_SEP: &str = ":";
|
||||
#[cfg(not(target_family = "unix"))]
|
||||
const ENV_SEP: &str = ";";
|
||||
|
||||
let mut by_op = HashMap::new(); // Paths as `String`s collected by `notify::op`
|
||||
let mut all_pathbufs = HashSet::new(); // All unique `PathBuf`s
|
||||
for pathop in pathops {
|
||||
if let Some(op) = pathop.op {
|
||||
// ignore pathops that don't have a `notify::op` set
|
||||
if let Some(s) = pathop.path.to_str() {
|
||||
// ignore invalid utf8 paths
|
||||
all_pathbufs.insert(pathop.path.clone());
|
||||
let e = by_op.entry(op).or_insert_with(Vec::new);
|
||||
e.push(s.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut vars = Vec::new();
|
||||
// Only break off a common path if we have more than one unique path,
|
||||
// otherwise we end up with a `COMMON_PATH` being set and other vars
|
||||
// being present but empty.
|
||||
let common_path = if all_pathbufs.len() > 1 {
|
||||
let all_pathbufs: Vec<PathBuf> = all_pathbufs.into_iter().collect();
|
||||
get_longest_common_path(&all_pathbufs)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(ref common_path) = common_path {
|
||||
vars.push(("WATCHEXEC_COMMON_PATH".to_string(), common_path.to_string()));
|
||||
}
|
||||
for (op, paths) in by_op {
|
||||
let key = match op {
|
||||
op if PathOp::is_create(op) => "WATCHEXEC_CREATED_PATH",
|
||||
op if PathOp::is_remove(op) => "WATCHEXEC_REMOVED_PATH",
|
||||
op if PathOp::is_rename(op) => "WATCHEXEC_RENAMED_PATH",
|
||||
op if PathOp::is_write(op) => "WATCHEXEC_WRITTEN_PATH",
|
||||
op if PathOp::is_meta(op) => "WATCHEXEC_META_CHANGED_PATH",
|
||||
_ => continue, // ignore `notify::op::RESCAN`s
|
||||
};
|
||||
|
||||
let paths = if let Some(ref common_path) = common_path {
|
||||
paths
|
||||
.iter()
|
||||
.map(|path_str| path_str.trim_start_matches(common_path).to_string())
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
paths
|
||||
};
|
||||
vars.push((key.to_string(), paths.as_slice().join(ENV_SEP)));
|
||||
}
|
||||
vars
|
||||
}
|
||||
|
||||
pub fn get_longest_common_path(paths: &[PathBuf]) -> Option<String> {
|
||||
match paths.len() {
|
||||
0 => return None,
|
||||
1 => return paths[0].to_str().map(ToString::to_string),
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let mut longest_path: Vec<_> = paths[0].components().collect();
|
||||
|
||||
for path in &paths[1..] {
|
||||
let mut greatest_distance = 0;
|
||||
for component_pair in path.components().zip(longest_path.iter()) {
|
||||
if component_pair.0 != *component_pair.1 {
|
||||
break;
|
||||
}
|
||||
|
||||
greatest_distance += 1;
|
||||
}
|
||||
|
||||
if greatest_distance != longest_path.len() {
|
||||
longest_path.truncate(greatest_distance);
|
||||
}
|
||||
}
|
||||
|
||||
let mut result = PathBuf::new();
|
||||
for component in longest_path {
|
||||
result.push(component.as_os_str());
|
||||
}
|
||||
|
||||
result.to_str().map(ToString::to_string)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::pathop::PathOp;
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::collect_path_env_vars;
|
||||
use super::get_longest_common_path;
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn longest_common_path_single_unix() {
|
||||
assert_eq!(
|
||||
get_longest_common_path(&[PathBuf::from("/tmp/random/")])
|
||||
.expect("failed to get longest common path"),
|
||||
"/tmp/random/"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn longest_common_path_similar_unix() {
|
||||
assert_eq!(
|
||||
get_longest_common_path(&[
|
||||
PathBuf::from("/tmp/logs/hi"),
|
||||
PathBuf::from("/tmp/logs/bye"),
|
||||
PathBuf::from("/tmp/logs/bye"),
|
||||
PathBuf::from("/tmp/logs/fly"),
|
||||
])
|
||||
.expect("failed to get longest common path"),
|
||||
"/tmp/logs"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn longest_common_path_divergent_unix() {
|
||||
assert_eq!(
|
||||
get_longest_common_path(&[
|
||||
PathBuf::from("/tmp/logs/hi"),
|
||||
PathBuf::from("/var/logs/hi")
|
||||
])
|
||||
.expect("failed to get longest common path"),
|
||||
"/"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn longest_common_path_uneven_unix() {
|
||||
assert_eq!(
|
||||
get_longest_common_path(&[
|
||||
PathBuf::from("/tmp/logs/hi"),
|
||||
PathBuf::from("/tmp/logs/"),
|
||||
PathBuf::from("/tmp/logs/bye"),
|
||||
])
|
||||
.expect("failed to get longest common path"),
|
||||
"/tmp/logs"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn longest_common_path_single_windows() {
|
||||
assert_eq!(
|
||||
get_longest_common_path(&[PathBuf::from(r"C:\Temp\Random\")])
|
||||
.expect("failed to get longest common path"),
|
||||
r"C:\Temp\Random\"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn longest_common_path_similar_windows() {
|
||||
assert_eq!(
|
||||
get_longest_common_path(&[
|
||||
PathBuf::from(r"C:\Temp\Logs\hi"),
|
||||
PathBuf::from(r"C:\Temp\Logs\bye"),
|
||||
PathBuf::from(r"C:\Temp\Logs\bye"),
|
||||
PathBuf::from(r"C:\Temp\Logs\fly"),
|
||||
])
|
||||
.expect("failed to get longest common path"),
|
||||
r"C:\Temp\Logs"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn longest_common_path_divergent_windows() {
|
||||
assert_eq!(
|
||||
get_longest_common_path(&[
|
||||
PathBuf::from(r"C:\Temp\Logs\hi"),
|
||||
PathBuf::from(r"C:\Perm\Logs\hi")
|
||||
])
|
||||
.expect("failed to get longest common path"),
|
||||
r"C:\"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn longest_common_path_uneven_windows() {
|
||||
assert_eq!(
|
||||
get_longest_common_path(&[
|
||||
PathBuf::from(r"C:\Temp\Logs\hi"),
|
||||
PathBuf::from(r"C:\Temp\Logs\"),
|
||||
PathBuf::from(r"C:\Temp\Logs\bye"),
|
||||
])
|
||||
.expect("failed to get longest common path"),
|
||||
r"C:\Temp\Logs"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn pathops_collect_to_env_vars_unix() {
|
||||
assert_eq!(
|
||||
collect_path_env_vars(&[
|
||||
PathOp::new(
|
||||
&PathBuf::from("/tmp/logs/hi"),
|
||||
Some(notify::op::CREATE),
|
||||
None,
|
||||
),
|
||||
PathOp::new(
|
||||
&PathBuf::from("/tmp/logs/hey/there"),
|
||||
Some(notify::op::CREATE),
|
||||
None,
|
||||
),
|
||||
PathOp::new(
|
||||
&PathBuf::from("/tmp/logs/bye"),
|
||||
Some(notify::op::REMOVE),
|
||||
None,
|
||||
),
|
||||
])
|
||||
.into_iter()
|
||||
.collect::<HashSet<_>>(),
|
||||
vec![
|
||||
("WATCHEXEC_COMMON_PATH".to_string(), "/tmp/logs".to_string()),
|
||||
("WATCHEXEC_REMOVED_PATH".to_string(), "/bye".to_string()),
|
||||
(
|
||||
"WATCHEXEC_CREATED_PATH".to_string(),
|
||||
"/hi:/hey/there".to_string(),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect::<HashSet<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn pathops_collect_to_env_vars_windows() {
|
||||
assert_eq!(
|
||||
collect_path_env_vars(&[
|
||||
PathOp::new(
|
||||
&PathBuf::from(r"C:\Temp\Logs\hi"),
|
||||
Some(notify::op::CREATE),
|
||||
None,
|
||||
),
|
||||
PathOp::new(
|
||||
&PathBuf::from(r"C:\Temp\Logs\hey\there"),
|
||||
Some(notify::op::CREATE),
|
||||
None,
|
||||
),
|
||||
PathOp::new(
|
||||
&PathBuf::from(r"C:\Temp\Logs\bye"),
|
||||
Some(notify::op::REMOVE),
|
||||
None,
|
||||
),
|
||||
])
|
||||
.into_iter()
|
||||
.collect::<HashSet<_>>(),
|
||||
vec![
|
||||
(
|
||||
"WATCHEXEC_COMMON_PATH".to_string(),
|
||||
r"C:\Temp\Logs".to_string()
|
||||
),
|
||||
("WATCHEXEC_REMOVED_PATH".to_string(), r"\bye".to_string()),
|
||||
(
|
||||
"WATCHEXEC_CREATED_PATH".to_string(),
|
||||
r"\hi;\hey\there".to_string(),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect::<HashSet<_>>()
|
||||
);
|
||||
}
|
||||
}
|
437
lib/src/run.rs
437
lib/src/run.rs
|
@ -1,437 +0,0 @@
|
|||
#[cfg(unix)]
|
||||
use command_group::UnixChildExt;
|
||||
use command_group::{CommandGroup, GroupChild};
|
||||
use log::{debug, info, warn};
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::canonicalize,
|
||||
process::Child,
|
||||
sync::{
|
||||
mpsc::{channel, Receiver},
|
||||
Arc, Mutex,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::gitignore;
|
||||
use crate::ignore;
|
||||
use crate::notification_filter::NotificationFilter;
|
||||
use crate::pathop::PathOp;
|
||||
use crate::signal::{self, Signal};
|
||||
use crate::watcher::{Event, Watcher};
|
||||
|
||||
/// Behaviour to use when handling updates while the command is running.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum OnBusyUpdate {
|
||||
/// ignore updates while busy
|
||||
DoNothing,
|
||||
|
||||
/// wait for the command to exit, then start a new one
|
||||
Queue,
|
||||
|
||||
/// restart the command immediately
|
||||
Restart,
|
||||
|
||||
/// send a signal only
|
||||
Signal,
|
||||
}
|
||||
|
||||
impl Default for OnBusyUpdate {
|
||||
fn default() -> Self {
|
||||
Self::DoNothing
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Handler {
|
||||
/// Called through a manual request, such as an initial run.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `Result` which means:
|
||||
///
|
||||
/// - `Err`: an error has occurred while processing, quit.
|
||||
/// - `Ok(true)`: everything is fine and the loop can continue.
|
||||
/// - `Ok(false)`: everything is fine but we should gracefully stop.
|
||||
fn on_manual(&self) -> Result<bool>;
|
||||
|
||||
/// Called through a file-update request.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// - `ops`: The list of events that triggered this update.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `Result` which means:
|
||||
///
|
||||
/// - `Err`: an error has occurred while processing, quit.
|
||||
/// - `Ok(true)`: everything is fine and the loop can continue.
|
||||
/// - `Ok(false)`: everything is fine but we should gracefully stop.
|
||||
fn on_update(&self, ops: &[PathOp]) -> Result<bool>;
|
||||
|
||||
/// Called once by `watch` at the very start.
|
||||
///
|
||||
/// Not called again; any changes will never be picked up.
|
||||
///
|
||||
/// The `Config` instance should be created using `ConfigBuilder` rather than direct initialisation
|
||||
/// to resist potential breaking changes (see semver policy on crate root).
|
||||
fn args(&self) -> Config;
|
||||
}
|
||||
|
||||
/// Starts watching, and calls a handler when something happens.
|
||||
///
|
||||
/// Given an argument structure and a `Handler` type, starts the watcher loop, blocking until done.
|
||||
pub fn watch<H>(handler: &H) -> Result<()>
|
||||
where
|
||||
H: Handler,
|
||||
{
|
||||
let args = handler.args();
|
||||
|
||||
let mut paths = vec![];
|
||||
for path in &args.paths {
|
||||
paths.push(
|
||||
canonicalize(&path)
|
||||
.map_err(|e| Error::Canonicalization(path.to_string_lossy().into_owned(), e))?,
|
||||
);
|
||||
}
|
||||
|
||||
let ignore = ignore::load(if args.no_ignore { &[] } else { &paths });
|
||||
let gitignore = gitignore::load(if args.no_vcs_ignore || args.no_ignore {
|
||||
&[]
|
||||
} else {
|
||||
&paths
|
||||
});
|
||||
let filter = NotificationFilter::new(&args.filters, &args.ignores, gitignore, ignore)?;
|
||||
|
||||
let (tx, rx) = channel();
|
||||
|
||||
#[cfg_attr(not(target_os = "linux"), allow(clippy::redundant_clone, unused_mut))]
|
||||
let mut maybe_watcher = Watcher::new(tx.clone(), &paths, args.poll, args.poll_interval);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if !args.poll {
|
||||
if let Err(notify::Error::Io(ref e)) = maybe_watcher {
|
||||
if e.raw_os_error() == Some(nix::libc::ENOSPC) {
|
||||
warn!("System notification limit is too small, falling back to polling mode. For better performance increase system limit:\n\tsysctl fs.inotify.max_user_watches=524288");
|
||||
maybe_watcher = Watcher::new(tx, &paths, true, args.poll_interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let watcher = maybe_watcher?;
|
||||
if watcher.is_polling() {
|
||||
warn!("Polling for changes every {:?}", args.poll_interval);
|
||||
}
|
||||
|
||||
// Call handler initially, if necessary
|
||||
if args.run_initially && !handler.on_manual()? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
loop {
|
||||
debug!("Waiting for filesystem activity");
|
||||
let paths = wait_fs(&rx, &filter, args.debounce, args.no_meta);
|
||||
info!("Paths updated: {:?}", paths);
|
||||
|
||||
if !handler.on_update(&paths)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ChildProcess {
|
||||
None,
|
||||
Grouped(GroupChild),
|
||||
Ungrouped(Child),
|
||||
}
|
||||
|
||||
impl Default for ChildProcess {
|
||||
fn default() -> Self {
|
||||
ChildProcess::None
|
||||
}
|
||||
}
|
||||
|
||||
impl ChildProcess {
|
||||
#[cfg(unix)]
|
||||
fn signal(&mut self, sig: Signal) -> Result<()> {
|
||||
match self {
|
||||
Self::None => Ok(()),
|
||||
Self::Grouped(c) => {
|
||||
debug!("Sending signal {} to process group id={}", sig, c.id());
|
||||
c.signal(sig)
|
||||
}
|
||||
Self::Ungrouped(c) => {
|
||||
debug!("Sending signal {} to process id={}", sig, c.id());
|
||||
c.signal(sig)
|
||||
}
|
||||
}
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn kill(&mut self) -> Result<()> {
|
||||
match self {
|
||||
Self::None => Ok(()),
|
||||
Self::Grouped(c) => {
|
||||
debug!("Killing process group id={}", c.id());
|
||||
c.kill()
|
||||
}
|
||||
Self::Ungrouped(c) => {
|
||||
debug!("Killing process id={}", c.id());
|
||||
c.kill()
|
||||
}
|
||||
}
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn is_running(&mut self) -> Result<bool> {
|
||||
match self {
|
||||
Self::None => Ok(false),
|
||||
Self::Grouped(c) => c.try_wait().map(|w| w.is_none()),
|
||||
Self::Ungrouped(c) => c.try_wait().map(|w| w.is_none()),
|
||||
}
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn wait(&mut self) -> Result<()> {
|
||||
match self {
|
||||
Self::None => Ok(()),
|
||||
Self::Grouped(c) => c.wait().map(drop),
|
||||
Self::Ungrouped(c) => c.wait().map(drop),
|
||||
}
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ExecHandler {
|
||||
args: Config,
|
||||
signal: Option<Signal>,
|
||||
child_process: Arc<Mutex<ChildProcess>>,
|
||||
}
|
||||
|
||||
impl ExecHandler {
|
||||
pub fn new(args: Config) -> Result<Self> {
|
||||
let child_process: Arc<Mutex<ChildProcess>> = Arc::default();
|
||||
let weak_child = Arc::downgrade(&child_process);
|
||||
|
||||
// Convert signal string to the corresponding integer
|
||||
let signal = signal::new(args.signal.clone());
|
||||
|
||||
signal::install_handler(move |sig: Signal| {
|
||||
if let Some(lock) = weak_child.upgrade() {
|
||||
let mut child = lock.lock().expect("poisoned lock in install_handler");
|
||||
match sig {
|
||||
Signal::SIGCHLD => {
|
||||
child.is_running().ok();
|
||||
}
|
||||
_ => {
|
||||
#[cfg(unix)]
|
||||
child.signal(sig).unwrap_or_else(|err| {
|
||||
warn!("Could not pass on signal to command: {}", err)
|
||||
});
|
||||
|
||||
#[cfg(not(unix))]
|
||||
child.kill().unwrap_or_else(|err| {
|
||||
warn!("Could not pass on termination to command: {}", err)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
args,
|
||||
signal,
|
||||
child_process,
|
||||
})
|
||||
}
|
||||
|
||||
fn spawn(&self, ops: &[PathOp]) -> Result<()> {
|
||||
if self.args.clear_screen {
|
||||
clearscreen::clear()?;
|
||||
}
|
||||
|
||||
let mut child = self.child_process.lock()?;
|
||||
child.kill().ok();
|
||||
|
||||
let mut command = self.args.shell.to_command(&self.args.cmd);
|
||||
debug!("Assembled command: {:?}", command);
|
||||
|
||||
if !self.args.no_environment {
|
||||
for (name, val) in crate::paths::collect_path_env_vars(ops) {
|
||||
debug!("Command environment: {}={:?}", name, val);
|
||||
command.env(name, val);
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Launching command");
|
||||
*child = if self.args.use_process_group {
|
||||
ChildProcess::Grouped(command.group_spawn()?)
|
||||
} else {
|
||||
ChildProcess::Ungrouped(command.spawn()?)
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn has_running_process(&self) -> Result<bool> {
|
||||
self.child_process
|
||||
.lock()
|
||||
.expect("poisoned lock in has_running_process")
|
||||
.is_running()
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler for ExecHandler {
|
||||
fn args(&self) -> Config {
|
||||
self.args.clone()
|
||||
}
|
||||
|
||||
// Only returns Err() on lock poisoning.
|
||||
fn on_manual(&self) -> Result<bool> {
|
||||
if self.args.once {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
self.spawn(&[])?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn on_update(&self, ops: &[PathOp]) -> Result<bool> {
|
||||
log::debug!("ON UPDATE: called");
|
||||
|
||||
let signal = self.signal.unwrap_or(Signal::SIGTERM);
|
||||
let has_running_processes = self.has_running_process()?;
|
||||
|
||||
log::debug!(
|
||||
"ON UPDATE: has_running_processes: {} --- on_busy_update: {:?}",
|
||||
has_running_processes,
|
||||
self.args.on_busy_update
|
||||
);
|
||||
|
||||
match (has_running_processes, self.args.on_busy_update) {
|
||||
// If nothing is running, start the command
|
||||
(false, _) => {
|
||||
self.spawn(ops)?;
|
||||
}
|
||||
|
||||
// Just send a signal to the command, do nothing more
|
||||
(true, OnBusyUpdate::Signal) => signal_process(&self.child_process, signal)?,
|
||||
|
||||
// Send a signal to the command, wait for it to exit, then run the command again
|
||||
(true, OnBusyUpdate::Restart) => {
|
||||
signal_process(&self.child_process, signal)?;
|
||||
wait_on_process(&self.child_process)?;
|
||||
self.spawn(ops)?;
|
||||
}
|
||||
|
||||
// Wait for the command to end, then run it again
|
||||
(true, OnBusyUpdate::Queue) => {
|
||||
wait_on_process(&self.child_process)?;
|
||||
self.spawn(ops)?;
|
||||
}
|
||||
|
||||
(true, OnBusyUpdate::DoNothing) => {}
|
||||
}
|
||||
|
||||
// Handle once option for integration testing
|
||||
if self.args.once {
|
||||
if let Some(signal) = self.signal {
|
||||
signal_process(&self.child_process, signal)?;
|
||||
}
|
||||
|
||||
wait_on_process(&self.child_process)?;
|
||||
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(args: Config) -> Result<()> {
|
||||
watch(&ExecHandler::new(args)?)
|
||||
}
|
||||
|
||||
fn wait_fs(
|
||||
rx: &Receiver<Event>,
|
||||
filter: &NotificationFilter,
|
||||
debounce: Duration,
|
||||
no_meta: bool,
|
||||
) -> Vec<PathOp> {
|
||||
let mut paths = Vec::new();
|
||||
let mut cache = HashMap::new();
|
||||
|
||||
loop {
|
||||
let e = rx.recv().expect("error when reading event");
|
||||
|
||||
if let Some(ref path) = e.path {
|
||||
let pathop = PathOp::new(path, e.op.ok(), e.cookie);
|
||||
if let Some(op) = pathop.op {
|
||||
if no_meta && PathOp::is_meta(op) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore cache for the initial file. Otherwise, in
|
||||
// debug mode it's hard to track what's going on
|
||||
let excluded = filter.is_excluded(path);
|
||||
if !cache.contains_key(&pathop) {
|
||||
cache.insert(pathop.clone(), excluded);
|
||||
}
|
||||
|
||||
if !excluded {
|
||||
paths.push(pathop);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for filesystem activity to cool off
|
||||
while let Ok(e) = rx.recv_timeout(debounce) {
|
||||
if let Some(ref path) = e.path {
|
||||
let pathop = PathOp::new(path, e.op.ok(), e.cookie);
|
||||
if cache.contains_key(&pathop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let excluded = filter.is_excluded(path);
|
||||
|
||||
cache.insert(pathop.clone(), excluded);
|
||||
|
||||
if !excluded {
|
||||
paths.push(pathop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paths
|
||||
}
|
||||
|
||||
fn signal_process(process: &Mutex<ChildProcess>, signal: Signal) -> Result<()> {
|
||||
let mut child = process.lock().expect("poisoned lock in signal_process");
|
||||
|
||||
#[cfg(unix)]
|
||||
child.signal(signal)?;
|
||||
|
||||
#[cfg(not(unix))]
|
||||
if matches!(signal, Signal::SIGTERM | Signal::SIGKILL) {
|
||||
child.kill()?;
|
||||
} else {
|
||||
debug!("Ignoring signal to send to process");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn wait_on_process(process: &Mutex<ChildProcess>) -> Result<()> {
|
||||
process
|
||||
.lock()
|
||||
.expect("poisoned lock in wait_on_process")
|
||||
.wait()
|
||||
}
|
197
lib/src/shell.rs
197
lib/src/shell.rs
|
@ -1,197 +0,0 @@
|
|||
use std::process::Command;
|
||||
|
||||
/// Shell to use to run commands.
|
||||
///
|
||||
/// `Cmd` and `Powershell` are special-cased because they have different calling
|
||||
/// conventions. Also `Cmd` is only available in Windows, while `Powershell` is
|
||||
/// also available on unices (provided the end-user has it installed, of course).
|
||||
///
|
||||
/// See [`Config.cmd`][crate::config::Config] for the semantics of `None` vs the
|
||||
/// other options.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Shell {
|
||||
/// Use no shell, and execute the command directly.
|
||||
None,
|
||||
|
||||
/// Use the given string as a unix shell invocation.
|
||||
///
|
||||
/// This means two things:
|
||||
/// - the program is invoked with `-c` followed by the command, and
|
||||
/// - the string will be split on space, and the resulting vec used as
|
||||
/// execvp(3) arguments: first is the shell program, rest are additional
|
||||
/// arguments (which come before the `-c` mentioned above). This is a very
|
||||
/// simplistic approach deliberately: it will not support quoted
|
||||
/// arguments, for example. Use [`Shell::None`] with a custom command vec
|
||||
/// if you want that.
|
||||
Unix(String),
|
||||
|
||||
/// Use the Windows CMD.EXE shell.
|
||||
///
|
||||
/// This is invoked with `/C` followed by the command.
|
||||
#[cfg(windows)]
|
||||
Cmd,
|
||||
|
||||
/// Use Powershell, on Windows or elsewhere.
|
||||
///
|
||||
/// This is invoked with `-Command` followed by the command.
|
||||
///
|
||||
/// This is preferred over `Unix("pwsh")`, though that will also work
|
||||
/// on unices due to Powershell supporting the `-c` short option.
|
||||
Powershell,
|
||||
}
|
||||
|
||||
impl Default for Shell {
|
||||
#[cfg(windows)]
|
||||
fn default() -> Self {
|
||||
Self::Powershell
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn default() -> Self {
|
||||
Self::Unix("sh".into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
/// Obtain a [`Command`] given the cmd vec from [`Config`][crate::config::Config].
|
||||
///
|
||||
/// Behaves as described in the enum documentation.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - Panics if `cmd` is empty.
|
||||
/// - Panics if the string in the `Unix` variant is empty or only whitespace.
|
||||
pub fn to_command(&self, cmd: &[String]) -> Command {
|
||||
assert!(!cmd.is_empty(), "cmd was empty");
|
||||
|
||||
match self {
|
||||
Shell::None => {
|
||||
// UNWRAP: checked by assert
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let (first, rest) = cmd.split_first().unwrap();
|
||||
let mut c = Command::new(first);
|
||||
c.args(rest);
|
||||
c
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
Shell::Cmd => {
|
||||
let mut c = Command::new("cmd.exe");
|
||||
c.arg("/C").arg(cmd.join(" "));
|
||||
c
|
||||
}
|
||||
|
||||
Shell::Powershell if cfg!(windows) => {
|
||||
let mut c = Command::new("powershell.exe");
|
||||
c.arg("-Command").arg(cmd.join(" "));
|
||||
c
|
||||
}
|
||||
|
||||
Shell::Powershell => {
|
||||
let mut c = Command::new("pwsh");
|
||||
c.arg("-Command").arg(cmd.join(" "));
|
||||
c
|
||||
}
|
||||
|
||||
Shell::Unix(name) => {
|
||||
assert!(!name.is_empty(), "shell program was empty");
|
||||
let sh = name.split_ascii_whitespace().collect::<Vec<_>>();
|
||||
|
||||
// UNWRAP: checked by assert
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let (shprog, shopts) = sh.split_first().unwrap();
|
||||
|
||||
let mut c = Command::new(shprog);
|
||||
c.args(shopts);
|
||||
c.arg("-c").arg(cmd.join(" "));
|
||||
c
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Shell;
|
||||
use command_group::CommandGroup;
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn unix_shell_default() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::default()
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status()?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn unix_shell_none() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::None
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status()?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn unix_shell_alternate() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::Unix("bash".into())
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status()?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn unix_shell_alternate_shopts() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::Unix("bash -o errexit".into())
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status()?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_shell_default() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::default()
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status()?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_shell_cmd() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::Cmd
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status()?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_shell_powershell() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::Powershell
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status()?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn windows_shell_unix_style_powershell() -> Result<(), std::io::Error> {
|
||||
assert!(Shell::Unix("powershell.exe".into())
|
||||
.to_command(&["echo".into(), "hi".into()])
|
||||
.group_status()?
|
||||
.success());
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,207 +0,0 @@
|
|||
use std::sync::Mutex;
|
||||
|
||||
type CleanupFn = Box<dyn Fn(self::Signal) + Send>;
|
||||
lazy_static::lazy_static! {
|
||||
static ref CLEANUP: Mutex<Option<CleanupFn>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
// Indicate interest in SIGCHLD by setting a dummy handler
|
||||
#[cfg(unix)]
|
||||
#[allow(clippy::missing_const_for_fn)]
|
||||
pub extern "C" fn sigchld_handler(_: c_int) {}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub use nix::sys::signal::Signal;
|
||||
|
||||
// This is a dummy enum for Windows
|
||||
#[cfg(windows)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Signal {
|
||||
SIGKILL,
|
||||
SIGTERM,
|
||||
SIGINT,
|
||||
SIGHUP,
|
||||
SIGSTOP,
|
||||
SIGCONT,
|
||||
SIGCHLD,
|
||||
SIGUSR1,
|
||||
SIGUSR2,
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::fmt;
|
||||
|
||||
#[cfg(windows)]
|
||||
impl fmt::Display for Signal {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::SIGKILL => "SIGKILL",
|
||||
Self::SIGTERM => "SIGTERM",
|
||||
Self::SIGINT => "SIGINT",
|
||||
Self::SIGHUP => "SIGHUP",
|
||||
Self::SIGSTOP => "SIGSTOP",
|
||||
Self::SIGCONT => "SIGCONT",
|
||||
Self::SIGCHLD => "SIGCHLD",
|
||||
Self::SIGUSR1 => "SIGUSR1",
|
||||
Self::SIGUSR2 => "SIGUSR2",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
use nix::libc::*;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub trait ConvertToLibc {
|
||||
fn convert_to_libc(self) -> c_int;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl ConvertToLibc for Signal {
|
||||
fn convert_to_libc(self) -> c_int {
|
||||
// Convert from signal::Signal enum to libc::* c_int constants
|
||||
match self {
|
||||
Self::SIGKILL => SIGKILL,
|
||||
Self::SIGTERM => SIGTERM,
|
||||
Self::SIGINT => SIGINT,
|
||||
Self::SIGHUP => SIGHUP,
|
||||
Self::SIGSTOP => SIGSTOP,
|
||||
Self::SIGCONT => SIGCONT,
|
||||
Self::SIGCHLD => SIGCHLD,
|
||||
Self::SIGUSR1 => SIGUSR1,
|
||||
Self::SIGUSR2 => SIGUSR2,
|
||||
_ => panic!("unsupported signal: {:?}", self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(signal_name: Option<String>) -> Option<Signal> {
|
||||
if let Some(signame) = signal_name {
|
||||
let signal = match signame.as_ref() {
|
||||
"SIGKILL" | "KILL" => Signal::SIGKILL,
|
||||
"SIGTERM" | "TERM" => Signal::SIGTERM,
|
||||
"SIGINT" | "INT" => Signal::SIGINT,
|
||||
"SIGHUP" | "HUP" => Signal::SIGHUP,
|
||||
"SIGSTOP" | "STOP" => Signal::SIGSTOP,
|
||||
"SIGCONT" | "CONT" => Signal::SIGCONT,
|
||||
"SIGCHLD" | "CHLD" => Signal::SIGCHLD,
|
||||
"SIGUSR1" | "USR1" => Signal::SIGUSR1,
|
||||
"SIGUSR2" | "USR2" => Signal::SIGUSR2,
|
||||
_ => panic!("unsupported signal: {}", signame),
|
||||
};
|
||||
|
||||
Some(signal)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn install_handler<F>(handler: F)
|
||||
where
|
||||
F: Fn(self::Signal) + 'static + Send + Sync,
|
||||
{
|
||||
use log::debug;
|
||||
use nix::sys::signal::*;
|
||||
use std::thread;
|
||||
|
||||
// Mask all signals interesting to us. The mask propagates
|
||||
// to all threads started after this point.
|
||||
let mut mask = SigSet::empty();
|
||||
mask.add(SIGKILL);
|
||||
mask.add(SIGTERM);
|
||||
mask.add(SIGINT);
|
||||
mask.add(SIGHUP);
|
||||
mask.add(SIGSTOP);
|
||||
mask.add(SIGCONT);
|
||||
mask.add(SIGCHLD);
|
||||
mask.add(SIGUSR1);
|
||||
mask.add(SIGUSR2);
|
||||
mask.thread_set_mask().expect("unable to set signal mask");
|
||||
|
||||
set_handler(handler);
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
let _ = sigaction(
|
||||
SIGCHLD,
|
||||
&SigAction::new(
|
||||
SigHandler::Handler(sigchld_handler),
|
||||
SaFlags::empty(),
|
||||
SigSet::empty(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Spawn a thread to catch these signals
|
||||
thread::spawn(move || {
|
||||
loop {
|
||||
let signal = mask.wait().expect("Unable to sigwait");
|
||||
debug!("Received {:?}", signal);
|
||||
|
||||
// Invoke closure
|
||||
invoke(signal);
|
||||
|
||||
// Restore default behavior for received signal and unmask it
|
||||
if signal != SIGCHLD {
|
||||
let default_action =
|
||||
SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty());
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
let _ = sigaction(signal, &default_action);
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_mask = SigSet::empty();
|
||||
new_mask.add(signal);
|
||||
|
||||
// Re-raise with signal unmasked
|
||||
let _ = new_mask.thread_unblock();
|
||||
let _ = raise(signal);
|
||||
let _ = new_mask.thread_block();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[allow(unsafe_code)]
|
||||
pub fn install_handler<F>(handler: F)
|
||||
where
|
||||
F: Fn(self::Signal) + 'static + Send + Sync,
|
||||
{
|
||||
use winapi::shared::minwindef::{BOOL, DWORD, FALSE, TRUE};
|
||||
use winapi::um::consoleapi::SetConsoleCtrlHandler;
|
||||
|
||||
pub unsafe extern "system" fn ctrl_handler(_: DWORD) -> BOOL {
|
||||
invoke(self::Signal::SIGTERM);
|
||||
|
||||
FALSE
|
||||
}
|
||||
|
||||
set_handler(handler);
|
||||
|
||||
unsafe {
|
||||
SetConsoleCtrlHandler(Some(ctrl_handler), TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
fn invoke(sig: self::Signal) {
|
||||
if let Some(ref handler) = *CLEANUP.lock().expect("poisoned lock in signal::invoke") {
|
||||
handler(sig)
|
||||
}
|
||||
}
|
||||
|
||||
fn set_handler<F>(handler: F)
|
||||
where
|
||||
F: Fn(self::Signal) + 'static + Send + Sync,
|
||||
{
|
||||
*CLEANUP
|
||||
.lock()
|
||||
.expect("poisoned lock in signal::set_handler") = Some(Box::new(handler));
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
use log::debug;
|
||||
use notify::{raw_watcher, PollWatcher, RecommendedWatcher, RecursiveMode};
|
||||
use std::convert::TryFrom;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Thin wrapper over the notify crate
|
||||
///
|
||||
/// `PollWatcher` and `RecommendedWatcher` are distinct types, but watchexec
|
||||
/// really just wants to handle them without regard to the exact type
|
||||
/// (e.g. polymorphically). This has the nice side effect of separating out
|
||||
/// all coupling to the notify crate into this module.
|
||||
pub struct Watcher {
|
||||
watcher_impl: WatcherImpl,
|
||||
}
|
||||
|
||||
pub use notify::Error;
|
||||
pub use notify::RawEvent as Event;
|
||||
|
||||
enum WatcherImpl {
|
||||
Recommended(RecommendedWatcher),
|
||||
Poll(PollWatcher),
|
||||
}
|
||||
|
||||
impl Watcher {
|
||||
pub fn new(
|
||||
tx: Sender<Event>,
|
||||
paths: &[PathBuf],
|
||||
poll: bool,
|
||||
interval: Duration,
|
||||
) -> Result<Self, Error> {
|
||||
use notify::Watcher;
|
||||
|
||||
let imp = if poll {
|
||||
let mut watcher = PollWatcher::with_delay_ms(
|
||||
tx,
|
||||
u32::try_from(interval.as_millis()).unwrap_or(u32::MAX),
|
||||
)?;
|
||||
for path in paths {
|
||||
watcher.watch(path, RecursiveMode::Recursive)?;
|
||||
debug!("Watching {:?}", path);
|
||||
}
|
||||
|
||||
WatcherImpl::Poll(watcher)
|
||||
} else {
|
||||
let mut watcher = raw_watcher(tx)?;
|
||||
for path in paths {
|
||||
watcher.watch(path, RecursiveMode::Recursive)?;
|
||||
debug!("Watching {:?}", path);
|
||||
}
|
||||
|
||||
WatcherImpl::Recommended(watcher)
|
||||
};
|
||||
|
||||
Ok(Self { watcher_impl: imp })
|
||||
}
|
||||
|
||||
pub fn is_polling(&self) -> bool {
|
||||
matches!(self.watcher_impl, WatcherImpl::Poll(_))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue